345 lines
12 KiB
Go
345 lines
12 KiB
Go
package handler
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"past/internal/httpapi"
|
||
"past/internal/store"
|
||
)
|
||
|
||
// mediaBase 对应 PHP 中 (defined('MEDIA_BASE') ? MEDIA_BASE : 'https://past.sundynix.cn/backend/'),MEDIA_BASE 未定义
|
||
const mediaBase = "https://past.sundynix.cn/backend/"
|
||
|
||
var tagRe = regexp.MustCompile(`<[^>]*>`)
|
||
|
||
// stripTags 近似 PHP strip_tags
|
||
func stripTags(s string) string { return tagRe.ReplaceAllString(s, "") }
|
||
|
||
// mbSubstr 对应 mb_substr($s, 0, n)
|
||
func mbSubstr(s string, n int) string {
|
||
r := []rune(s)
|
||
if len(r) > n {
|
||
return string(r[:n])
|
||
}
|
||
return s
|
||
}
|
||
|
||
// avatarURL 对应评论里的 avatar_url 拼接
|
||
func avatarURL(avatar string) string {
|
||
if avatar == "" {
|
||
return ""
|
||
}
|
||
if strings.HasPrefix(avatar, "http") {
|
||
return avatar
|
||
}
|
||
return mediaBase + avatar
|
||
}
|
||
|
||
// diaryDateOrFallback 对应 PHP !empty($input['diary_date']) 的日期默认逻辑。
|
||
// 缺失、null、空字符串和纯空白都不允许进入 diary_date NOT NULL 列。
|
||
func diaryDateOrFallback(value any, fallback string) string {
|
||
if date := strings.TrimSpace(store.ToStr(value)); date != "" {
|
||
return date
|
||
}
|
||
return fallback
|
||
}
|
||
|
||
func registerDiary(r *httpapi.Router) {
|
||
r.Handle("diary_list", diaryList)
|
||
r.Handle("diary_add", diaryAdd)
|
||
r.Handle("diary_update", diaryUpdate)
|
||
r.Handle("diary_delete", diaryDelete)
|
||
r.Handle("diary_detail", diaryDetail)
|
||
r.Handle("diary_comment_list", diaryCommentList)
|
||
r.Handle("diary_comment_add", diaryCommentAdd)
|
||
r.Handle("diary_comment_delete", diaryCommentDelete)
|
||
}
|
||
|
||
func diaryList(c *httpapi.Ctx) error {
|
||
ids := coupleUserIds(c, 0)
|
||
if len(ids) == 0 {
|
||
demo := demoData("diary")
|
||
return c.OK(M{"items": demo, "total": len(demo)})
|
||
}
|
||
page := c.QInt("page")
|
||
if !c.QHas("page") {
|
||
page = 1
|
||
}
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
pageSize := int64(10)
|
||
if c.QHas("page_size") {
|
||
pageSize = c.QInt("page_size")
|
||
}
|
||
if pageSize < 1 {
|
||
pageSize = 1
|
||
}
|
||
if pageSize > 50 {
|
||
pageSize = 50
|
||
}
|
||
offset := (page - 1) * pageSize
|
||
cnt, err := c.DB.QueryOne("SELECT COUNT(*) as cnt FROM diaries WHERE created_by IN (?, ?)", ids[0], ids[1])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
items, err := c.DB.Query(`SELECT d.*,
|
||
(SELECT COUNT(*) FROM diary_comments WHERE diary_id = d.id) as comment_count,
|
||
(SELECT COUNT(*) FROM diary_likes WHERE diary_id = d.id) as like_count,
|
||
(SELECT COUNT(*) FROM diary_favorites WHERE diary_id = d.id) as favorite_count,
|
||
(SELECT COUNT(*) FROM diary_likes WHERE diary_id = d.id AND user_id = ?) > 0 as is_liked,
|
||
(SELECT COUNT(*) FROM diary_favorites WHERE diary_id = d.id AND user_id = ?) > 0 as is_favorited
|
||
FROM diaries d WHERE d.created_by IN (?, ?) ORDER BY d.diary_date DESC, d.created_at DESC LIMIT ? OFFSET ?`,
|
||
uid, uid, ids[0], ids[1], pageSize, offset)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, it := range items {
|
||
it["images"] = jsonDecodeList(it["images"])
|
||
it["is_liked"] = store.Truthy(it["is_liked"])
|
||
it["is_favorited"] = store.Truthy(it["is_favorited"])
|
||
}
|
||
return c.OK(M{"items": rows(items), "total": cnt["cnt"], "page": page, "page_size": pageSize})
|
||
}
|
||
|
||
func diaryAdd(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
cid, err := requireCoupleID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
diaryDate := diaryDateOrFallback(c.In("diary_date"), today())
|
||
images := inOr(c, "images", []any{})
|
||
title := inOr(c, "title", "")
|
||
content := inOr(c, "content", "")
|
||
titleS, contentS := store.ToStr(title), store.ToStr(content)
|
||
region := c.InTrim("region")
|
||
lat, lng := c.InFloat("lat"), c.InFloat("lng")
|
||
location := c.InTrim("location")
|
||
if lat != 0 && lng != 0 && location == "" {
|
||
location = c.App.Geo.ReverseGeocode(lat, lng)
|
||
}
|
||
if err := checkTextOr400(c, strings.TrimSpace(titleS+" "+contentS)); err != nil {
|
||
return err
|
||
}
|
||
id, err := c.DB.Insert("INSERT INTO diaries (couple_id, title, content, mood, weather, diary_date, images, is_anniversary, region, lat, lng, location, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||
cid, title, content, inOr(c, "mood", "😊"), inOr(c, "weather", "☀️"), diaryDate, jsonEncode(images), inOr(c, "is_anniversary", 0), region, lat, lng, location, uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
imgCount := 0
|
||
if l, ok := images.([]any); ok {
|
||
imgCount = len(l)
|
||
}
|
||
desc := "发表了日记"
|
||
if store.Truthy(titleS) {
|
||
desc += "「" + titleS + "」"
|
||
}
|
||
if imgCount > 0 {
|
||
desc += fmt.Sprintf(",上传了%d张图片", imgCount)
|
||
}
|
||
actID := logActivity(c.DB, cid, uid, "diary_create", desc, "")
|
||
if partnerID := partnerUserId(c.DB, uid); partnerID != 0 {
|
||
displayTitle := "一篇新日记"
|
||
if store.Truthy(titleS) {
|
||
displayTitle = titleS
|
||
}
|
||
displayTitle = mbTruncate(displayTitle, 20)
|
||
displayContent := "TA写了一些心里话"
|
||
if store.Truthy(contentS) {
|
||
displayContent = mbSubstr(stripTags(contentS), 50)
|
||
}
|
||
c.App.Wechat.SendSubscribeMessage(partnerID, "notify", map[string]string{
|
||
"thing1": displayTitle, "thing2": displayContent, "date3": today(), "thing4": "快去看看吧 💕",
|
||
}, "pages/diary/diary", actID, uid)
|
||
}
|
||
return c.OK(M{"success": true, "id": fmt.Sprint(id)})
|
||
}
|
||
|
||
func diaryUpdate(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
ids, err := requireCoupleUserIds(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
images := inOr(c, "images", []any{})
|
||
// 旧版小程序的编辑页不传 diary_date。空值时保留数据库原日期;
|
||
// 若历史上已被临时改成可空且存在 NULL,则回填当天,保证不再写入 NULL。
|
||
diaryDate := strings.TrimSpace(c.InStr("diary_date"))
|
||
dateExpr := "COALESCE(NULLIF(?, ''), NULLIF(diary_date, ''), ?)"
|
||
if err := checkTextOr400(c, strings.TrimSpace(c.InStr("title")+" "+c.InStr("content"))); err != nil {
|
||
return err
|
||
}
|
||
if c.InHas("lat") {
|
||
_, err = c.DB.Exec("UPDATE diaries SET title = ?, content = ?, mood = ?, weather = ?, diary_date = "+dateExpr+", images = ?, is_anniversary = ?, region = ?, lat = ?, lng = ?, location = ? WHERE id = ? AND created_by IN (?, ?)",
|
||
c.In("title"), inOr(c, "content", ""), inOr(c, "mood", "😊"), inOr(c, "weather", "☀️"), diaryDate, today(), jsonEncode(images), inOr(c, "is_anniversary", 0), c.InTrim("region"), c.InFloat("lat"), c.InFloat("lng"), c.InTrim("location"), c.In("id"), ids[0], ids[1])
|
||
} else {
|
||
_, err = c.DB.Exec("UPDATE diaries SET title = ?, content = ?, mood = ?, weather = ?, diary_date = "+dateExpr+", images = ?, is_anniversary = ?, region = ? WHERE id = ? AND created_by IN (?, ?)",
|
||
c.In("title"), inOr(c, "content", ""), inOr(c, "mood", "😊"), inOr(c, "weather", "☀️"), diaryDate, today(), jsonEncode(images), inOr(c, "is_anniversary", 0), c.InTrim("region"), c.In("id"), ids[0], ids[1])
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return c.OK(M{"success": true})
|
||
}
|
||
|
||
func diaryDelete(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
ids, err := requireCoupleUserIds(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := c.DB.Exec("DELETE FROM diaries WHERE id = ? AND created_by IN (?, ?)", c.In("id"), ids[0], ids[1]); err != nil {
|
||
return err
|
||
}
|
||
return c.OK(M{"success": true})
|
||
}
|
||
|
||
func countOf(c *httpapi.Ctx, sql string, args ...any) int64 {
|
||
r, _ := c.DB.QueryOne(sql, args...)
|
||
if r == nil {
|
||
return 0
|
||
}
|
||
return r.Int("cnt")
|
||
}
|
||
|
||
func diaryDetail(c *httpapi.Ctx) error {
|
||
id := c.QInt("id")
|
||
if id == 0 {
|
||
return c.Err(400, "缺少日记ID")
|
||
}
|
||
ids, err := requireCoupleUserIds(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
item, err := c.DB.QueryOne("SELECT d.*, u.nickname as creator_name, u.avatar as creator_avatar FROM diaries d LEFT JOIN users u ON d.created_by = u.id WHERE d.id = ? AND d.created_by IN (?, ?)", id, ids[0], ids[1])
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if item == nil {
|
||
return c.Err(404, "日记不存在")
|
||
}
|
||
item["images"] = jsonDecodeList(item["images"])
|
||
item["comment_count"] = countOf(c, "SELECT COUNT(*) as cnt FROM diary_comments WHERE diary_id = ?", id)
|
||
if _, err := c.DB.QueryOne("SELECT COUNT(*) as cnt FROM diary_likes WHERE diary_id = ?", id); err != nil {
|
||
return err
|
||
}
|
||
item["like_count"] = countOf(c, "SELECT COUNT(*) as cnt FROM diary_likes WHERE diary_id = ?", id)
|
||
item["favorite_count"] = countOf(c, "SELECT COUNT(*) as cnt FROM diary_favorites WHERE diary_id = ?", id)
|
||
item["is_liked"] = countOf(c, "SELECT COUNT(*) as cnt FROM diary_likes WHERE diary_id = ? AND user_id = ?", id, uid) > 0
|
||
item["is_favorited"] = countOf(c, "SELECT COUNT(*) as cnt FROM diary_favorites WHERE diary_id = ? AND user_id = ?", id, uid) > 0
|
||
return c.OK(item)
|
||
}
|
||
|
||
func diaryCommentList(c *httpapi.Ctx) error {
|
||
diaryID := c.QInt("diary_id")
|
||
if diaryID == 0 {
|
||
return c.Err(400, "缺少日记ID")
|
||
}
|
||
ids, err := requireCoupleUserIds(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
suffix, window := listWindow(c)
|
||
comments, err := c.DB.Query("SELECT c.*, u.nickname, u.avatar FROM diary_comments c LEFT JOIN users u ON c.user_id = u.id WHERE c.diary_id = ? AND c.couple_id IN (SELECT couple_id FROM users WHERE id IN (?, ?)) ORDER BY c.created_at DESC, c.id DESC"+suffix, append([]any{diaryID, ids[0], ids[1]}, window...)...)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, cm := range comments {
|
||
cm["avatar_url"] = avatarURL(cm.Str("avatar"))
|
||
}
|
||
return listResponse(c, comments, countC(c, "SELECT COUNT(*) as c FROM diary_comments WHERE diary_id=? AND couple_id IN (SELECT couple_id FROM users WHERE id IN (?, ?))", diaryID, ids[0], ids[1]), nil)
|
||
}
|
||
|
||
func diaryCommentAdd(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
cid, err := requireCoupleID(c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
if uid == 0 {
|
||
return c.Err(401, "请先登录")
|
||
}
|
||
diaryID := c.InInt("diary_id")
|
||
content := c.InTrim("content")
|
||
if diaryID == 0 {
|
||
return c.Err(400, "缺少日记ID")
|
||
}
|
||
if !store.Truthy(content) {
|
||
return c.Err(400, "请输入评论内容")
|
||
}
|
||
if err := checkTextOr400(c, content); err != nil {
|
||
return err
|
||
}
|
||
diary, _ := c.DB.QueryOne("SELECT id, created_by FROM diaries WHERE id = ? AND couple_id = ?", diaryID, cid)
|
||
if diary == nil {
|
||
return c.Err(404, "日记不存在")
|
||
}
|
||
parentID := c.InInt("parent_id")
|
||
var replyToUserID any
|
||
if c.InHas("reply_to_user_id") {
|
||
replyToUserID = c.InInt("reply_to_user_id")
|
||
}
|
||
commentID, err := c.DB.Insert("INSERT INTO diary_comments (diary_id, couple_id, user_id, content, parent_id, reply_to_user_id, reply_to_username) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||
diaryID, cid, uid, content, parentID, replyToUserID, c.InTrim("reply_to_username"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
comment, _ := c.DB.QueryOne("SELECT c.*, u.nickname, u.avatar FROM diary_comments c LEFT JOIN users u ON c.user_id = u.id WHERE c.id = ?", commentID)
|
||
var out any = false
|
||
if comment != nil {
|
||
comment["avatar_url"] = avatarURL(comment.Str("avatar"))
|
||
out = comment
|
||
}
|
||
// 通知对象:优先被回复的人,否则日记作者;自己评论自己不通知
|
||
var notifyTo int64
|
||
if replyToUserID != nil {
|
||
notifyTo = store.ToInt(replyToUserID)
|
||
}
|
||
if notifyTo == 0 {
|
||
notifyTo = diary.Int("created_by")
|
||
}
|
||
if notifyTo != 0 && notifyTo != uid {
|
||
actID := logActivity(c.DB, cid, uid, "diary_comment_add", "评论了你的日记", "")
|
||
c.App.Wechat.SendSubscribeMessage(notifyTo, "notify", map[string]string{
|
||
"thing1": "日记有新评论 💬",
|
||
"thing2": mbTruncate("TA评论了你:"+content, 20),
|
||
"date3": today(),
|
||
"thing4": "点击查看详情",
|
||
}, "pages/diary/diary", actID, uid)
|
||
}
|
||
return c.OK(M{"success": true, "comment": out})
|
||
}
|
||
|
||
func diaryCommentDelete(c *httpapi.Ctx) error {
|
||
if err := c.RequirePOST(); err != nil {
|
||
return err
|
||
}
|
||
uid := c.UID()
|
||
if uid == 0 {
|
||
return c.Err(401, "请先登录")
|
||
}
|
||
commentID := c.InInt("id")
|
||
if commentID == 0 {
|
||
return c.Err(400, "缺少评论ID")
|
||
}
|
||
if _, err := c.DB.Exec("DELETE FROM diary_comments WHERE id = ? AND user_id = ?", commentID, uid); err != nil {
|
||
return err
|
||
}
|
||
return c.OK(M{"success": true})
|
||
}
|