Files
sundynix-pets/pets-be/internal/service/community.go
T
Blizzard 3fde90e97b feat: 补齐五块缺失功能(记录丢数据/文章正文/提醒管理/我的页入口/分享/关注)
P0 修数据问题
- 记录弹层有 5 个输入框从未绑定,用户填的便便备注、饮食内容、药品名称、
  剂量、消费备注全部丢弃;用药记录因此只存下「用药记录」四个字。已全部绑定并入库
- 异常观察保存时写死「风险中」,AI 实际判定的等级被丢掉。改为使用 riskData
  的真实 risk_level,并把成因与建议存进备注

文章正文
- 早期 seed 的 4 篇文章 Content 全空,点开只是打开关联记录弹层,内容运营等于白做
- 补写 4 篇真实正文(疫苗驱虫/软便判断/换粮方案/养宠花销),遵循不诊断、
  异常必提就医的护栏;对已存在的空正文做幂等回填
- 新增文章详情页,学习页与首页卡片改为打开正文

提醒管理
- 后端 CRUD 早已具备但小程序只读,用户改不了疫苗驱虫日期。提醒弹层支持增删改
- 修复后端 UpdateReminder 漏掉 type 字段导致改类型不生效

「我的」页 7 个死入口接活
- 提醒设置、健康记录、多宠物管理、会员权益分别落到对应功能
- 数据导出:汇总档案与记录为文本,可复制发给医生
- 意见反馈:新增 feedback 表与提交接口,后台加「意见反馈」管理页
- 关于我们:应用说明与免责声明

分享
- 各页面补 onShareAppMessage/onShareTimeline
- 帖子分享按钮改用微信原生 open-type="share",删除原来点了没反应的空壳弹层

社区关注
- 新增 follows 表与关注/取关接口,「关注」tab 此前等同「推荐」,
  现只显示已关注用户的帖子;帖子头部可直接关注,标记 followed / is_self

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:11:08 +08:00

230 lines
6.2 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 service
import (
"encoding/json"
"gorm.io/datatypes"
"gorm.io/gorm"
"github.com/sundynix/pets-be/internal/model"
)
// attachPostImages 把帖子的 ImageFileIDs 解析为可展示的 ImagesURL 数组)。
// 机器人帖 ImageFileIDs 为空,保留其 Imagesemoji)不变。
func (s *Service) attachPostImages(posts []model.Post) {
perPost := make([][]string, len(posts))
all := []string{}
for i := range posts {
if len(posts[i].ImageFileIDs) == 0 {
continue
}
var ids []string
if json.Unmarshal(posts[i].ImageFileIDs, &ids) == nil && len(ids) > 0 {
perPost[i] = ids
all = append(all, ids...)
}
}
if len(all) == 0 {
return
}
urlMap := s.fileURLs(all)
for i := range posts {
if len(perPost[i]) == 0 {
continue
}
urls := make([]string, 0, len(perPost[i]))
for _, id := range perPost[i] {
if u := urlMap[id]; u != "" {
urls = append(urls, u)
}
}
if b, err := json.Marshal(urls); err == nil {
posts[i].Images = datatypes.JSON(b)
}
}
}
// tabTag 将 feed tab 映射为标签过滤(空表示不过滤)
func tabTag(tab string) string {
switch tab {
case "新手求助":
return "求助"
case "晒宠":
return "晒宠"
case "经验":
return "经验"
default: // 推荐 / 关注
return ""
}
}
// ListPosts 帖子分页列表。userID 用于「关注」tab 过滤和标记关注状态(可为空)。
func (s *Service) ListPosts(userID, tab string, offset, limit int) ([]model.Post, int64, error) {
q := s.db.Model(&model.Post{}).Where("status = ?", model.PostPublished)
if tag := tabTag(tab); tag != "" {
q = q.Where("JSON_CONTAINS(tags, ?)", `"`+tag+`"`)
}
if tab == "关注" {
ids := s.followeeIDs(userID)
if len(ids) == 0 {
// 一个都没关注:返回空列表,而不是退化成「推荐」
return []model.Post{}, 0, nil
}
q = q.Where("user_id IN ?", ids)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var posts []model.Post
if err := q.Order("id desc").Offset(offset).Limit(limit).Find(&posts).Error; err != nil {
return nil, 0, err
}
s.attachPostImages(posts)
s.markFollowed(userID, posts)
return posts, total, nil
}
// GetPost 帖子详情
func (s *Service) GetPost(postID string) (*model.Post, error) {
var p model.Post
if err := s.db.First(&p, postID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrNotFound
}
return nil, err
}
one := []model.Post{p}
s.attachPostImages(one)
return &one[0], nil
}
// PostInput 发帖入参
type PostInput struct {
PetID *string
Identity string
Content string
Tags datatypes.JSON
Images datatypes.JSON
ImageFileIDs datatypes.JSON
}
// CreatePost 发帖
func (s *Service) CreatePost(userID string, in PostInput) (*model.Post, error) {
authorName := "匿名宠友"
authorEmoji := "🐾"
switch in.Identity {
case "official":
authorName = "肉垫计划官方"
case "petName":
if in.PetID != nil {
var pet model.Pet
if err := s.db.First(&pet, *in.PetID).Error; err == nil {
authorName = pet.Name + "的铲屎官"
authorEmoji = pet.Emoji
}
}
}
p := model.Post{
UserID: userID, PetID: in.PetID, AuthorName: authorName, AuthorEmoji: authorEmoji,
Identity: in.Identity, Content: in.Content, Tags: in.Tags, Images: in.Images,
ImageFileIDs: in.ImageFileIDs, Status: model.PostPublished,
}
if err := s.db.Create(&p).Error; err != nil {
return nil, err
}
one := []model.Post{p}
s.attachPostImages(one)
return &one[0], nil
}
// LikePost 点赞(幂等:已赞则不重复计数)
func (s *Service) LikePost(userID, postID string) (int, error) {
if _, err := s.GetPost(postID); err != nil {
return 0, err
}
err := s.db.Transaction(func(tx *gorm.DB) error {
like := model.PostLike{PostID: postID, UserID: userID}
res := tx.Where("post_id = ? AND user_id = ?", postID, userID).FirstOrCreate(&like)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 1 { // 新建才计数
return tx.Model(&model.Post{}).Where("id = ?", postID).
UpdateColumn("like_count", gorm.Expr("like_count + 1")).Error
}
return nil
})
if err != nil {
return 0, err
}
return s.postLikeCount(postID)
}
// UnlikePost 取消点赞
func (s *Service) UnlikePost(userID, postID string) (int, error) {
if _, err := s.GetPost(postID); err != nil {
return 0, err
}
err := s.db.Transaction(func(tx *gorm.DB) error {
res := tx.Where("post_id = ? AND user_id = ?", postID, userID).Delete(&model.PostLike{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 1 {
return tx.Model(&model.Post{}).Where("id = ? AND like_count > 0", postID).
UpdateColumn("like_count", gorm.Expr("like_count - 1")).Error
}
return nil
})
if err != nil {
return 0, err
}
return s.postLikeCount(postID)
}
func (s *Service) postLikeCount(postID string) (int, error) {
var p model.Post
if err := s.db.Select("like_count").First(&p, postID).Error; err != nil {
return 0, err
}
return p.LikeCount, nil
}
// ListComments 评论分页
func (s *Service) ListComments(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 {
return nil, 0, err
}
var comments []model.Comment
if err := q.Order("id desc").Offset(offset).Limit(limit).Find(&comments).Error; err != nil {
return nil, 0, err
}
return comments, total, nil
}
// CreateComment 评论
func (s *Service) CreateComment(userID, postID string, content string) (*model.Comment, error) {
if _, err := s.GetPost(postID); err != nil {
return nil, err
}
var user model.User
name := "宠友"
if err := s.db.First(&user, userID).Error; err == nil && user.Nickname != "" {
name = user.Nickname
}
comment := model.Comment{PostID: postID, UserID: userID, AuthorName: name, Content: content, Status: "published"}
if err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&comment).Error; err != nil {
return err
}
return tx.Model(&model.Post{}).Where("id = ?", postID).
UpdateColumn("comment_count", gorm.Expr("comment_count + 1")).Error
}); err != nil {
return nil, err
}
return &comment, nil
}