feat:增加分享音乐功能

This commit is contained in:
amos
2025-12-30 11:13:49 +08:00
parent 598be33bac
commit 3389cc98ba
13 changed files with 651 additions and 10 deletions

View File

@@ -13,11 +13,11 @@ android {
applicationId = "com.memory.app"
minSdk = 26
targetSdk = 35
versionCode = 24
versionName = "1.4.0"
versionCode = 27
versionName = "1.4.3"
buildConfigField("String", "API_BASE_URL", "\"https://x.amos.us.kg/api/\"")
buildConfigField("int", "VERSION_CODE", "24")
buildConfigField("int", "VERSION_CODE", "27")
}
signingConfigs {

View File

@@ -25,12 +25,24 @@ data class Post(
@SerialName("updated_at") val updatedAt: String? = null,
val user: User? = null,
val media: List<Media> = emptyList(),
val music: MusicInfo? = null,
val reactions: List<ReactionGroup> = emptyList(),
@SerialName("like_count") val likeCount: Int = 0,
val liked: Boolean = false,
@SerialName("comment_count") val commentCount: Int = 0
)
@Serializable
data class MusicInfo(
val id: Long,
@SerialName("post_id") val postId: Long,
val title: String,
val artist: String,
val cover: String = "",
@SerialName("share_url") val shareUrl: String,
val platform: String = "qqmusic"
)
@Serializable
data class Media(
val id: Long,
@@ -87,7 +99,8 @@ data class LoginResponse(
data class CreatePostRequest(
val content: String,
@SerialName("media_ids") val mediaIds: List<String> = emptyList(),
val visibility: Int = 0 // 0=所有人可见, 1=仅自己可见
val visibility: Int = 0, // 0=所有人可见, 1=仅自己可见
@SerialName("music_url") val musicUrl: String? = null
)
@Serializable

View File

@@ -0,0 +1,198 @@
package com.memory.app.ui.components
import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import coil.compose.AsyncImage
import com.memory.app.data.model.MusicInfo
import com.memory.app.ui.theme.Brand500
@Composable
fun MusicCard(
music: MusicInfo,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Column {
// 歌曲信息行
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { isExpanded = !isExpanded }
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 封面图
Box(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
if (music.cover.isNotEmpty()) {
AsyncImage(
model = music.cover,
contentDescription = "专辑封面",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
// 播放按钮遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Black.copy(alpha = 0.3f),
Color.Transparent
)
)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "播放",
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
} else {
Icon(
imageVector = Icons.Default.MusicNote,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.width(12.dp))
// 歌曲信息
Column(modifier = Modifier.weight(1f)) {
Text(
text = music.title,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = music.artist,
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.width(8.dp))
// 展开/收起图标
Icon(
imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = if (isExpanded) "收起" else "展开播放器",
tint = Brand500,
modifier = Modifier.size(24.dp)
)
}
// 内嵌播放器
AnimatedVisibility(
visible = isExpanded,
enter = expandVertically(),
exit = shrinkVertically()
) {
EmbeddedMusicPlayer(
url = music.shareUrl,
modifier = Modifier
.fillMaxWidth()
.height(65.dp)
.padding(horizontal = 12.dp)
.padding(bottom = 12.dp)
)
}
}
}
}
@SuppressLint("SetJavaScriptEnabled")
@Composable
private fun EmbeddedMusicPlayer(
url: String,
modifier: Modifier = Modifier
) {
AndroidView(
factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
useWideViewPort = true
loadWithOverviewMode = true
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
}
webViewClient = WebViewClient()
webChromeClient = WebChromeClient()
// 设置透明背景
setBackgroundColor(android.graphics.Color.TRANSPARENT)
loadUrl(url)
}
},
modifier = modifier.clip(RoundedCornerShape(8.dp))
)
}

View File

@@ -61,6 +61,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import com.memory.app.data.api.ApiClient
import com.memory.app.data.model.MusicInfo
import com.memory.app.data.model.Post
import com.memory.app.data.model.UserInfoResponse
import com.memory.app.ui.theme.*
@@ -76,6 +77,7 @@ fun PostCard(
onCommentClick: () -> Unit,
onReactionClick: (String) -> Unit,
onEditClick: ((Post) -> Unit)? = null,
onMusicClick: ((MusicInfo) -> Unit)? = null,
modifier: Modifier = Modifier
) {
var showEmojiPicker by remember { mutableStateOf(false) }
@@ -188,6 +190,15 @@ fun PostCard(
)
}
// Music Card
if (post.music != null) {
Spacer(modifier = Modifier.height(12.dp))
MusicCard(
music = post.music,
onClick = { onMusicClick?.invoke(post.music) }
)
}
// Reactions display
if (post.reactions.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))

View File

@@ -200,6 +200,7 @@ fun MainNavigation(
onImageClick = { }
)
}
}
// Bottom Navigation
@@ -261,8 +262,8 @@ fun MainNavigation(
CreatePostScreen(
user = user,
onClose = { showCreatePost = false },
onPost = { content, images, visibility ->
homeViewModel.createPost(context, content, images, visibility)
onPost = { content, images, visibility, musicUrl ->
homeViewModel.createPost(context, content, images, visibility, musicUrl)
},
isLoading = isPosting
)

View File

@@ -12,8 +12,10 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.MusicNote
import androidx.compose.material.icons.outlined.Mood
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.*
@@ -34,7 +36,7 @@ import com.memory.app.ui.theme.*
fun CreatePostScreen(
user: User?,
onClose: () -> Unit,
onPost: (String, List<Uri>, Int) -> Unit, // 添加 visibility 参数
onPost: (String, List<Uri>, Int, String?) -> Unit, // content, images, visibility, musicUrl
isLoading: Boolean
) {
// 拦截系统返回手势
@@ -46,6 +48,8 @@ fun CreatePostScreen(
var selectedImages by remember { mutableStateOf<List<Uri>>(emptyList()) }
var showEmojiPicker by remember { mutableStateOf(false) }
var visibility by remember { mutableStateOf(0) } // 0=所有人可见, 1=仅自己可见
var musicUrl by remember { mutableStateOf("") }
var showMusicInput by remember { mutableStateOf(false) }
val imagePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetMultipleContents()
@@ -83,7 +87,7 @@ fun CreatePostScreen(
// Publish button
Button(
onClick = { onPost(content, selectedImages, visibility) },
onClick = { onPost(content, selectedImages, visibility, musicUrl.ifBlank { null }) },
enabled = content.isNotBlank() && !isLoading,
shape = RoundedCornerShape(50),
colors = ButtonDefaults.buttonColors(
@@ -211,6 +215,46 @@ fun CreatePostScreen(
}
}
}
// Music Link Display
if (musicUrl.isNotBlank()) {
Spacer(modifier = Modifier.height(12.dp))
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.MusicNote,
contentDescription = null,
tint = Brand500,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "已添加音乐链接",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = { musicUrl = "" },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "移除",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
}
@@ -242,6 +286,20 @@ fun CreatePostScreen(
)
}
// Music Button
IconButton(
onClick = { showMusicInput = true },
enabled = musicUrl.isBlank(),
modifier = Modifier.size(40.dp)
) {
Icon(
Icons.Outlined.MusicNote,
contentDescription = "添加音乐",
tint = if (musicUrl.isBlank()) Brand500 else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
// Emoji Button
IconButton(
onClick = { showEmojiPicker = true },
@@ -312,6 +370,17 @@ fun CreatePostScreen(
}
)
}
// Music Link Input Dialog
if (showMusicInput) {
MusicLinkInputDialog(
onDismiss = { showMusicInput = false },
onConfirm = { url ->
musicUrl = url
showMusicInput = false
}
)
}
}
@Composable
@@ -445,3 +514,97 @@ private fun CreatePostEmojiPicker(
}
}
}
@Composable
private fun MusicLinkInputDialog(
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
) {
var inputUrl by remember { mutableStateOf("") }
val isValidUrl = inputUrl.contains("y.qq.com") || inputUrl.contains("qq.com")
androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) {
Surface(
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surface,
shadowElevation = 16.dp,
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(24.dp)) {
// 标题
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(4.dp, 24.dp)
.clip(RoundedCornerShape(2.dp))
.background(Brand500)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "添加音乐",
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "粘贴 QQ 音乐分享链接",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
// 输入框
OutlinedTextField(
value = inputUrl,
onValueChange = { inputUrl = it },
placeholder = {
Text(
"https://y.qq.com/...",
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Brand500,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
),
singleLine = true
)
Spacer(modifier = Modifier.height(20.dp))
// 按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onDismiss) {
Text(
"取消",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = { onConfirm(inputUrl) },
enabled = isValidUrl,
colors = ButtonDefaults.buttonColors(
containerColor = Brand500,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
),
shape = RoundedCornerShape(12.dp)
) {
Text("确定", fontWeight = FontWeight.Bold)
}
}
}
}
}
}

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.memory.app.data.model.Post
import com.memory.app.ui.components.MusicCard
import com.memory.app.ui.theme.*
@Composable
@@ -213,6 +214,15 @@ fun EditPostScreen(
}
}
}
// Music Card - 显示已有音乐(编辑时不可修改)
if (post.music != null) {
Spacer(modifier = Modifier.height(12.dp))
MusicCard(
music = post.music,
onClick = { }
)
}
}
}

View File

@@ -28,6 +28,7 @@ import com.memory.app.data.model.Comment
import com.memory.app.data.model.Post
import com.memory.app.ui.components.EmojiPickerDialog
import com.memory.app.ui.components.FullScreenImageViewer
import com.memory.app.ui.components.MusicCard
import com.memory.app.ui.theme.*
import com.memory.app.util.TimeUtils
@@ -204,6 +205,15 @@ fun PostDetailScreen(
}
}
// Music Card
if (post.music != null) {
Spacer(modifier = Modifier.height(12.dp))
MusicCard(
music = post.music,
onClick = { }
)
}
Spacer(modifier = Modifier.height(16.dp))
// Stats Row

View File

@@ -190,7 +190,7 @@ class HomeViewModel : ViewModel() {
loadPosts()
}
fun createPost(context: Context, content: String, images: List<Uri>, visibility: Int = 0) {
fun createPost(context: Context, content: String, images: List<Uri>, visibility: Int = 0, musicUrl: String? = null) {
if (_isPosting.value) return
viewModelScope.launch {
_isPosting.value = true
@@ -217,7 +217,8 @@ class HomeViewModel : ViewModel() {
val request = CreatePostRequest(
content = content,
mediaIds = imageUrls,
visibility = visibility
visibility = visibility,
musicUrl = musicUrl
)
val response = ApiClient.api.createPost(request)
if (response.isSuccessful) {

View File

@@ -169,5 +169,20 @@ func migrate(db *sql.DB) error {
db.Exec("CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read)")
// 迁移:创建音乐分享表
db.Exec(`
CREATE TABLE IF NOT EXISTS post_music (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL UNIQUE,
title TEXT NOT NULL,
artist TEXT NOT NULL,
cover TEXT DEFAULT '',
share_url TEXT NOT NULL,
platform TEXT DEFAULT 'qqmusic',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
)
`)
return nil
}

View File

@@ -0,0 +1,176 @@
package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
)
// QQ音乐链接解析
type QQMusicParser struct{}
type MusicParseResult struct {
Title string `json:"title"`
Artist string `json:"artist"`
Cover string `json:"cover"`
ShareURL string `json:"share_url"`
Platform string `json:"platform"`
SongID string `json:"song_id"` // 新增歌曲ID用于生成外链播放器
}
// ParseQQMusicURL 解析QQ音乐分享链接
func (p *QQMusicParser) Parse(url string) (*MusicParseResult, error) {
// 支持的链接格式:
// 1. https://c6.y.qq.com/base/fcgi-bin/u?__=xxx (短链接)
// 2. https://y.qq.com/n/ryqq/songDetail/xxx (歌曲详情页)
// 3. https://i.y.qq.com/v8/playsong.html?songmid=xxx (播放页)
// 先获取最终URL处理短链接重定向
finalURL, err := p.getFinalURL(url)
if err != nil {
return nil, err
}
// 提取歌曲ID (songmid)
songMid := p.extractSongMid(finalURL)
if songMid == "" {
// 尝试从原始URL提取
songMid = p.extractSongMid(url)
}
if songMid == "" {
return nil, fmt.Errorf("无法解析歌曲ID")
}
// 获取歌曲信息
return p.getSongInfo(songMid, finalURL)
}
func (p *QQMusicParser) getFinalURL(url string) (string, error) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(url)
if err != nil {
return url, nil
}
defer resp.Body.Close()
if resp.StatusCode == 301 || resp.StatusCode == 302 {
location := resp.Header.Get("Location")
if location != "" {
return location, nil
}
}
return url, nil
}
func (p *QQMusicParser) extractSongMid(url string) string {
// 匹配 songmid=xxx 或 songDetail/xxx
patterns := []string{
`songmid=([a-zA-Z0-9]+)`,
`songDetail/([a-zA-Z0-9]+)`,
`song/([a-zA-Z0-9]+)`,
`ADBE=([a-zA-Z0-9]+)`,
}
for _, pattern := range patterns {
re := regexp.MustCompile(pattern)
matches := re.FindStringSubmatch(url)
if len(matches) > 1 {
return matches[1]
}
}
return ""
}
func (p *QQMusicParser) getSongInfo(songMid string, shareURL string) (*MusicParseResult, error) {
// 使用QQ音乐的公开API获取歌曲信息
apiURL := fmt.Sprintf("https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg?songmid=%s&format=json", songMid)
req, _ := http.NewRequest("GET", apiURL, nil)
req.Header.Set("Referer", "https://y.qq.com/")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result struct {
Code int `json:"code"`
Data []struct {
ID int `json:"id"` // 数字ID用于外链播放器
Name string `json:"name"`
Album struct {
Mid string `json:"mid"`
Name string `json:"name"`
} `json:"album"`
Singer []struct {
Name string `json:"name"`
} `json:"singer"`
} `json:"data"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if result.Code != 0 || len(result.Data) == 0 {
return nil, fmt.Errorf("获取歌曲信息失败")
}
song := result.Data[0]
// 拼接歌手名
var artists []string
for _, s := range song.Singer {
artists = append(artists, s.Name)
}
// 专辑封面
cover := ""
if song.Album.Mid != "" {
cover = fmt.Sprintf("https://y.qq.com/music/photo_new/T002R300x300M000%s.jpg", song.Album.Mid)
}
// 构建外链播放器URL使用数字songid
// 格式: https://i.y.qq.com/n2/m/outchain/player/index.html?songid=127570280&songtype=0
playerURL := fmt.Sprintf("https://i.y.qq.com/n2/m/outchain/player/index.html?songid=%d&songtype=0", song.ID)
return &MusicParseResult{
Title: song.Name,
Artist: strings.Join(artists, " / "),
Cover: cover,
ShareURL: playerURL, // 使用外链播放器URL
SongID: fmt.Sprintf("%d", song.ID),
Platform: "qqmusic",
}, nil
}
// IsQQMusicURL 检查是否是QQ音乐链接
func IsQQMusicURL(url string) bool {
patterns := []string{
`y\.qq\.com`,
`c6\.y\.qq\.com`,
`i\.y\.qq\.com`,
}
for _, pattern := range patterns {
if matched, _ := regexp.MatchString(pattern, url); matched {
return true
}
}
return false
}

View File

@@ -57,6 +57,22 @@ func (h *PostHandler) Create(c *gin.Context) {
}
}
// 处理音乐链接
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
@@ -135,6 +151,8 @@ func (h *PostHandler) List(c *gin.Context) {
post.Media = h.getPostMedia(post.ID)
// 获取表情反应
post.Reactions = h.getPostReactions(post.ID, userID)
// 获取音乐信息
post.Music = h.getPostMusic(post.ID)
posts = append(posts, post)
}
@@ -182,6 +200,7 @@ func (h *PostHandler) Get(c *gin.Context) {
}
post.Media = h.getPostMedia(post.ID)
post.Reactions = h.getPostReactions(post.ID, userID)
post.Music = h.getPostMusic(post.ID)
c.JSON(http.StatusOK, post)
}
@@ -412,3 +431,15 @@ func (h *PostHandler) getPostReactions(postID, userID int64) []model.ReactionGro
}
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
}

View File

@@ -23,12 +23,23 @@ type Post struct {
UpdatedAt *time.Time `json:"updated_at,omitempty"`
User *User `json:"user,omitempty"`
Media []Media `json:"media,omitempty"`
Music *MusicInfo `json:"music,omitempty"`
Reactions []ReactionGroup `json:"reactions,omitempty"`
LikeCount int `json:"like_count"`
Liked bool `json:"liked"`
CommentCount int `json:"comment_count"`
}
type MusicInfo struct {
ID int64 `json:"id"`
PostID int64 `json:"post_id"`
Title string `json:"title"`
Artist string `json:"artist"`
Cover string `json:"cover"`
ShareURL string `json:"share_url"`
Platform string `json:"platform"` // qqmusic, netease, etc.
}
type Media struct {
ID int64 `json:"id"`
PostID int64 `json:"post_id"`
@@ -89,6 +100,7 @@ type CreatePostRequest struct {
Content string `json:"content" binding:"required,max=1000"`
MediaIDs []string `json:"media_ids"`
Visibility int `json:"visibility"` // 0=所有人可见, 1=仅自己可见
MusicURL string `json:"music_url"` // QQ音乐分享链接
}
type UpdatePostRequest struct {