1125 lines
32 KiB
Go
1125 lines
32 KiB
Go
package legacy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
filePb "sundynix-micro-go/app/file/rpc/file"
|
|
aiLogic "sundynix-micro-go/app/plant/api/internal/logic/ai"
|
|
"sundynix-micro-go/app/plant/api/internal/logic/complete"
|
|
plantLogic "sundynix-micro-go/app/plant/api/internal/logic/myPlant"
|
|
ocrLogic "sundynix-micro-go/app/plant/api/internal/logic/ocr"
|
|
postLogic "sundynix-micro-go/app/plant/api/internal/logic/post"
|
|
topicLogic "sundynix-micro-go/app/plant/api/internal/logic/topic"
|
|
wikiLogic "sundynix-micro-go/app/plant/api/internal/logic/wiki"
|
|
"sundynix-micro-go/app/plant/api/internal/svc"
|
|
"sundynix-micro-go/app/plant/api/internal/types"
|
|
plantModel "sundynix-micro-go/app/plant/model"
|
|
plantPb "sundynix-micro-go/app/plant/rpc/plant"
|
|
"sundynix-micro-go/common/response"
|
|
|
|
"github.com/zeromicro/go-zero/rest/httpx"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func idFromQuery(r *http.Request) string {
|
|
if id := r.URL.Query().Get("id"); id != "" {
|
|
return id
|
|
}
|
|
if id := r.URL.Query().Get("wikiId"); id != "" {
|
|
return id
|
|
}
|
|
if id := r.URL.Query().Get("postId"); id != "" {
|
|
return id
|
|
}
|
|
if id := r.URL.Query().Get("itemId"); id != "" {
|
|
return id
|
|
}
|
|
return r.URL.Query().Get("classId")
|
|
}
|
|
|
|
func fileListByIDs(r *http.Request, svcCtx *svc.ServiceContext, ids []string) []map[string]interface{} {
|
|
if len(ids) == 0 {
|
|
return []map[string]interface{}{}
|
|
}
|
|
resp, err := svcCtx.FileRpc.GetFilesByIds(r.Context(), &filePb.GetFilesByIdsReq{Ids: ids})
|
|
if err != nil {
|
|
return []map[string]interface{}{}
|
|
}
|
|
list := make([]map[string]interface{}, 0, len(resp.Files))
|
|
for _, f := range resp.Files {
|
|
list = append(list, map[string]interface{}{
|
|
"id": f.Id, "name": f.Name, "url": f.Url, "tag": f.Tag,
|
|
"key": f.Key, "suffix": f.Suffix, "md5": f.Md5, "createdAt": f.CreatedAt,
|
|
})
|
|
}
|
|
return list
|
|
}
|
|
|
|
func PlantDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var myPlant plantModel.MyPlant
|
|
if err := svcCtx.DB.
|
|
Preload("CarePlans").
|
|
Preload("CareRecords", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("created_at desc")
|
|
}).
|
|
Preload("GrowthRecords", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("created_at desc")
|
|
}).
|
|
Where("id = ?", idFromQuery(r)).First(&myPlant).Error; err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
|
|
// 查图片(本地关联表 + FileRpc)
|
|
imgList := queryImagesByPlant(svcCtx, r.Context(), myPlant.ID)
|
|
|
|
// 成长记录图片
|
|
growthImgMap := queryGrowthImages(svcCtx, r.Context(), myPlant.GrowthRecords)
|
|
|
|
response.OkWithData(w, map[string]interface{}{
|
|
"plant": toPlantMap(myPlant),
|
|
"imgList": imgList,
|
|
"carePlans": toPlanMapList(myPlant.CarePlans),
|
|
"careRecords": toRecordMapList(myPlant.CareRecords),
|
|
"careTasks": nil,
|
|
"growthRecords": toGrowthMapList(myPlant.GrowthRecords, growthImgMap),
|
|
})
|
|
}
|
|
}
|
|
|
|
func DeletePlanHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
l := plantLogic.NewDeleteCarePlanLogic(r.Context(), svcCtx)
|
|
if err := l.DeleteCarePlan(&types.IdsReq{Ids: []string{idFromQuery(r)}}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func WikiDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
resp, err := svcCtx.PlantRpc.GetWikiDetail(r.Context(), &plantPb.IdReq{Id: idFromQuery(r)})
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
if resp != nil && resp.Wiki != nil {
|
|
// 通过 FileRpc 获取完整图片信息
|
|
imgList := fetchFileMap(svcCtx, r.Context(), resp.Wiki.OssIds)
|
|
response.OkWithData(w, map[string]interface{}{
|
|
"wiki": resp.Wiki, "imgList": imgListToList(imgList, resp.Wiki.OssIds),
|
|
"classIds": resp.Wiki.ClassIds, "relatedWikiIds": resp.Wiki.RelatedWikiIds,
|
|
})
|
|
return
|
|
}
|
|
response.OkWithData(w, resp)
|
|
}
|
|
}
|
|
|
|
func WikiPageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req types.WikiListReq
|
|
if err := httpx.Parse(r, &req); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
|
|
resp, err := svcCtx.PlantRpc.GetWikiList(r.Context(), &plantPb.WikiListReq{
|
|
Current: int32(req.Current),
|
|
PageSize: int32(req.PageSize),
|
|
Name: req.Name,
|
|
ClassId: req.ClassId,
|
|
IsHot: int32(req.IsHot),
|
|
})
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
|
|
// 查询所有 wiki 的图片
|
|
var wikiIds []string
|
|
for _, w := range resp.List {
|
|
wikiIds = append(wikiIds, w.Id)
|
|
}
|
|
|
|
// 查本地关联表获取 OSS ID
|
|
type rel struct {
|
|
WikiID string `gorm:"column:wiki_id"`
|
|
OssID string `gorm:"column:oss_id"`
|
|
}
|
|
var rels []rel
|
|
if len(wikiIds) > 0 {
|
|
svcCtx.DB.Table("sundynix_plant_wiki_oss").
|
|
Where("wiki_id IN ?", wikiIds).
|
|
Find(&rels)
|
|
}
|
|
wikiOssMap := make(map[string][]string)
|
|
var allOssIds []string
|
|
for _, r := range rels {
|
|
wikiOssMap[r.WikiID] = append(wikiOssMap[r.WikiID], r.OssID)
|
|
allOssIds = append(allOssIds, r.OssID)
|
|
}
|
|
|
|
// 通过 FileRpc 获取图片信息
|
|
fileMap := fetchFileMap(svcCtx, r.Context(), allOssIds)
|
|
|
|
// 组装返回
|
|
var list []map[string]interface{}
|
|
for _, w := range resp.List {
|
|
ossIds := wikiOssMap[w.Id]
|
|
imgList := imgListToList(fileMap, ossIds)
|
|
hasStarVal := 0
|
|
if w.IsStar {
|
|
hasStarVal = 1
|
|
}
|
|
list = append(list, map[string]interface{}{
|
|
"id": w.Id, "name": w.Name, "latinName": w.LatinName,
|
|
"aliases": w.Aliases, "genus": w.Genus, "difficulty": w.Difficulty,
|
|
"isHot": w.IsHot, "growthHabit": w.GrowthHabit,
|
|
"lightIntensity": w.LightIntensity, "classId": w.ClassId,
|
|
"createdAt": w.CreatedAt, "hasStar": hasStarVal,
|
|
"imgList": imgList,
|
|
})
|
|
}
|
|
if list == nil {
|
|
list = []map[string]interface{}{}
|
|
}
|
|
|
|
response.OkWithData(w, map[string]interface{}{
|
|
"list": list, "total": resp.Total,
|
|
})
|
|
}
|
|
}
|
|
|
|
// imgListToList 将 fileMap 按顺序转为列表
|
|
func imgListToList(fileMap map[string]map[string]interface{}, ossIds []string) []map[string]interface{} {
|
|
var list []map[string]interface{}
|
|
for _, id := range ossIds {
|
|
if img, ok := fileMap[id]; ok {
|
|
list = append(list, img)
|
|
}
|
|
}
|
|
if list == nil {
|
|
list = []map[string]interface{}{}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func WikiStarHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
l := wikiLogic.NewToggleWikiStarLogic(r.Context(), svcCtx)
|
|
if err := l.ToggleWikiStar(&types.IdReq{Id: idFromQuery(r)}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func TopicDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
l := topicLogic.NewGetTopicDetailLogic(r.Context(), svcCtx)
|
|
resp, err := l.GetTopicDetail(&types.IdPathReq{Id: idFromQuery(r)})
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.OkWithData(w, resp)
|
|
}
|
|
}
|
|
|
|
func LikePostHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
l := postLogic.NewLikePostLogic(r.Context(), svcCtx)
|
|
if err := l.LikePost(&types.IdReq{Id: idFromQuery(r)}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func StarPostHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
l := postLogic.NewStarPostLogic(r.Context(), svcCtx)
|
|
if err := l.StarPost(&types.IdReq{Id: idFromQuery(r)}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func ExchangeItemDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
l := complete.NewGetExchangeItemDetailLogic(r.Context(), svcCtx)
|
|
resp, err := l.GetExchangeItemDetail(&types.IdPathReq{Id: idFromQuery(r)})
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.OkWithData(w, resp)
|
|
}
|
|
}
|
|
|
|
func PostPageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req types.PostListReq
|
|
if err := httpx.Parse(r, &req); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
userId := fmt.Sprintf("%v", r.Context().Value("userId"))
|
|
|
|
db := svcCtx.DB.Model(&plantModel.Post{})
|
|
if req.Keyword != "" {
|
|
db = db.Where("title like ? OR content like ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
|
|
}
|
|
|
|
var total int64
|
|
db.Count(&total)
|
|
pageSize := req.PageSize
|
|
if pageSize <= 0 {
|
|
pageSize = 20
|
|
}
|
|
page := req.Current
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
|
|
var posts []*plantModel.Post
|
|
if err := db.
|
|
Preload("CommentList", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("created_at asc")
|
|
}).
|
|
Preload("LikeList").
|
|
Limit(pageSize).Offset(offset).Order("created_at desc").Find(&posts).Error; err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
|
|
// 组装完整响应
|
|
list := buildPostListResponse(svcCtx, r.Context(), posts, userId)
|
|
response.OkWithData(w, map[string]interface{}{
|
|
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
|
})
|
|
}
|
|
}
|
|
|
|
func MyPostPageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req types.PostListReq
|
|
if err := httpx.Parse(r, &req); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
userId := fmt.Sprintf("%v", r.Context().Value("userId"))
|
|
|
|
db := svcCtx.DB.Model(&plantModel.Post{}).Where("user_id = ?", userId)
|
|
|
|
var total int64
|
|
db.Count(&total)
|
|
pageSize := req.PageSize
|
|
if pageSize <= 0 {
|
|
pageSize = 20
|
|
}
|
|
page := req.Current
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
offset := (page - 1) * pageSize
|
|
|
|
var posts []*plantModel.Post
|
|
if err := db.
|
|
Preload("CommentList", func(db *gorm.DB) *gorm.DB {
|
|
return db.Order("created_at asc")
|
|
}).
|
|
Preload("LikeList").
|
|
Limit(pageSize).Offset(offset).Order("created_at desc").Find(&posts).Error; err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
|
|
list := buildPostListResponse(svcCtx, r.Context(), posts, userId)
|
|
response.OkWithData(w, map[string]interface{}{
|
|
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ========== Post 列表辅助函数 ==========
|
|
|
|
// buildPostListResponse 组装完整帖子列表响应
|
|
func buildPostListResponse(svcCtx *svc.ServiceContext, ctx context.Context, posts []*plantModel.Post, userId string) []map[string]interface{} {
|
|
if len(posts) == 0 {
|
|
return []map[string]interface{}{}
|
|
}
|
|
|
|
var postIds []string
|
|
for _, p := range posts {
|
|
postIds = append(postIds, p.ID)
|
|
}
|
|
|
|
// 1. 查帖子图片(本地关联表 + FileRpc)
|
|
postImgMap := queryPostImages(svcCtx, ctx, postIds)
|
|
|
|
// 2. 查用户信息
|
|
allUserIds := collectPostUserIds(posts)
|
|
userMap := queryUserMapV2(svcCtx, allUserIds)
|
|
|
|
// 3. 查当前用户点赞/收藏状态
|
|
likedSet, starredSet := queryLikeStarStatus(svcCtx, userId, postIds)
|
|
|
|
// 4. 组装
|
|
var list []map[string]interface{}
|
|
for _, p := range posts {
|
|
item := map[string]interface{}{
|
|
"id": p.ID, "title": p.Title, "content": p.Content,
|
|
"userId": p.UserID, "location": p.Location,
|
|
"viewCount": p.ViewCount, "commentCount": p.CommentCount,
|
|
"likeCount": p.LikeCount, "starCount": p.StarCount,
|
|
"hasReviewed": p.HasReviewed,
|
|
"createdAt": p.CreatedAt.Format(time.RFC3339),
|
|
"updatedAt": p.UpdatedAt.Format(time.RFC3339),
|
|
"createdAtStr": p.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
"hasLiked": 0, "hasStar": 0,
|
|
"imgList": postImgMap[p.ID],
|
|
"publisher": buildPublisherInfo(userMap, p.UserID),
|
|
"commentList": buildCommentList(userMap, p.CommentList),
|
|
"likeList": buildLikeList(userMap, p.LikeList),
|
|
"starList": []map[string]interface{}{},
|
|
}
|
|
if likedSet[p.ID] {
|
|
item["hasLiked"] = 1
|
|
}
|
|
if starredSet[p.ID] {
|
|
item["hasStar"] = 1
|
|
}
|
|
list = append(list, item)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func collectPostUserIds(posts []*plantModel.Post) []string {
|
|
set := make(map[string]bool)
|
|
for _, p := range posts {
|
|
set[p.UserID] = true
|
|
for _, c := range p.CommentList {
|
|
set[c.UserID] = true
|
|
}
|
|
for _, l := range p.LikeList {
|
|
set[l.UserID] = true
|
|
}
|
|
}
|
|
var ids []string
|
|
for id := range set {
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// queryPostImages 查询帖子图片
|
|
func queryPostImages(svcCtx *svc.ServiceContext, ctx context.Context, postIds []string) map[string][]map[string]interface{} {
|
|
type rel struct {
|
|
PostID string `gorm:"column:post_id"`
|
|
OssID string `gorm:"column:oss_id"`
|
|
}
|
|
var rels []rel
|
|
svcCtx.DB.Table("sundynix_plant_post_oss").
|
|
Where("post_id IN ?", postIds).
|
|
Find(&rels)
|
|
|
|
var allOssIds []string
|
|
pidMap := make(map[string][]string)
|
|
for _, r := range rels {
|
|
pidMap[r.PostID] = append(pidMap[r.PostID], r.OssID)
|
|
allOssIds = append(allOssIds, r.OssID)
|
|
}
|
|
|
|
// 通过 FileRpc 获取文件信息
|
|
fileInfos := fetchFileMap(svcCtx, ctx, allOssIds)
|
|
|
|
result := make(map[string][]map[string]interface{})
|
|
for pid, ids := range pidMap {
|
|
var imgs []map[string]interface{}
|
|
for _, oid := range ids {
|
|
if info, ok := fileInfos[oid]; ok {
|
|
imgs = append(imgs, info)
|
|
}
|
|
}
|
|
if imgs == nil {
|
|
imgs = []map[string]interface{}{}
|
|
}
|
|
result[pid] = imgs
|
|
}
|
|
// 没有图片的帖子也返回空数组
|
|
for _, pid := range postIds {
|
|
if _, ok := result[pid]; !ok {
|
|
result[pid] = []map[string]interface{}{}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// fetchFileMap 通过 FileRpc 获取文件信息
|
|
func fetchFileMap(svcCtx *svc.ServiceContext, ctx context.Context, ids []string) map[string]map[string]interface{} {
|
|
result := make(map[string]map[string]interface{})
|
|
if len(ids) == 0 {
|
|
return result
|
|
}
|
|
resp, err := svcCtx.FileRpc.GetFilesByIds(ctx, &filePb.GetFilesByIdsReq{Ids: ids})
|
|
if err != nil || resp == nil {
|
|
return result
|
|
}
|
|
for _, f := range resp.Files {
|
|
result[f.Id] = map[string]interface{}{
|
|
"id": f.Id, "name": f.Name, "url": f.Url, "tag": f.Tag,
|
|
"key": f.Key, "suffix": f.Suffix, "md5": f.Md5,
|
|
"createdAt": unixToTimeStr(f.CreatedAt),
|
|
"updatedAt": unixToTimeStr(f.CreatedAt),
|
|
"createdAtStr": unixToTimeStrShort(f.CreatedAt),
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func unixToTimeStr(ts int64) string {
|
|
if ts == 0 {
|
|
return ""
|
|
}
|
|
return time.Unix(ts, 0).Format(time.RFC3339)
|
|
}
|
|
|
|
func unixToTimeStrShort(ts int64) string {
|
|
if ts == 0 {
|
|
return ""
|
|
}
|
|
return time.Unix(ts, 0).Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
// queryUserMapV2 查询用户信息
|
|
func queryUserMapV2(svcCtx *svc.ServiceContext, ids []string) map[string]map[string]interface{} {
|
|
result := make(map[string]map[string]interface{})
|
|
if len(ids) == 0 {
|
|
return result
|
|
}
|
|
type userRow struct {
|
|
ID string `gorm:"column:id"`
|
|
NickName string `gorm:"column:nick_name"`
|
|
Name string `gorm:"column:name"`
|
|
AvatarID string `gorm:"column:avatar_id"`
|
|
}
|
|
var rows []userRow
|
|
svcCtx.DB.Table("sundynix_user").Where("id IN ?", ids).Find(&rows)
|
|
|
|
var avatarIds []string
|
|
for _, row := range rows {
|
|
if row.AvatarID != "" {
|
|
avatarIds = append(avatarIds, row.AvatarID)
|
|
}
|
|
}
|
|
// 头像通过 FileRpc 获取
|
|
avatarMap := fetchFileMap(svcCtx, context.Background(), avatarIds)
|
|
|
|
for _, row := range rows {
|
|
avatarData := map[string]interface{}{}
|
|
if av, ok := avatarMap[row.AvatarID]; ok {
|
|
avatarData = av
|
|
}
|
|
result[row.ID] = map[string]interface{}{
|
|
"id": row.ID, "nickName": row.NickName, "name": row.Name,
|
|
"avatarId": row.AvatarID, "avatar": avatarData,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// queryLikeStarStatus 查询当前用户的点赞/收藏状态
|
|
func queryLikeStarStatus(svcCtx *svc.ServiceContext, userId string, postIds []string) (likedSet, starredSet map[string]bool) {
|
|
likedSet = make(map[string]bool)
|
|
starredSet = make(map[string]bool)
|
|
if len(postIds) == 0 {
|
|
return
|
|
}
|
|
type rel struct {
|
|
PostID string `gorm:"column:post_id"`
|
|
}
|
|
var likes []rel
|
|
svcCtx.DB.Table("sundynix_plant_post_like").
|
|
Where("post_id IN ? AND user_id = ?", postIds, userId).Find(&likes)
|
|
for _, l := range likes {
|
|
likedSet[l.PostID] = true
|
|
}
|
|
var stars []rel
|
|
svcCtx.DB.Table("sundynix_plant_user_star").
|
|
Where("target_id IN ? AND user_id = ? AND type = 'post'", postIds, userId).Find(&stars)
|
|
for _, s := range stars {
|
|
starredSet[s.PostID] = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func buildPublisherInfo(userMap map[string]map[string]interface{}, userId string) map[string]interface{} {
|
|
if u, ok := userMap[userId]; ok {
|
|
return u
|
|
}
|
|
return map[string]interface{}{
|
|
"id": userId, "nickName": "", "name": "", "avatarId": "", "avatar": map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
func buildCommentList(userMap map[string]map[string]interface{}, comments []*plantModel.PostComment) []map[string]interface{} {
|
|
var list []map[string]interface{}
|
|
for _, c := range comments {
|
|
list = append(list, map[string]interface{}{
|
|
"id": c.ID, "postId": c.PostID, "userId": c.UserID,
|
|
"content": c.Content, "parentId": c.ParentID,
|
|
"createdAt": c.CreatedAt.Format(time.RFC3339),
|
|
"updatedAt": c.UpdatedAt.Format(time.RFC3339),
|
|
"createdAtStr": c.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
"commentator": buildPublisherInfo(userMap, c.UserID),
|
|
})
|
|
}
|
|
if list == nil {
|
|
list = []map[string]interface{}{}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func buildLikeList(userMap map[string]map[string]interface{}, likes []*plantModel.PostLike) []map[string]interface{} {
|
|
var list []map[string]interface{}
|
|
for _, l := range likes {
|
|
list = append(list, map[string]interface{}{
|
|
"id": l.ID, "postId": l.PostID, "userId": l.UserID,
|
|
"liker": buildPublisherInfo(userMap, l.UserID),
|
|
})
|
|
}
|
|
if list == nil {
|
|
list = []map[string]interface{}{}
|
|
}
|
|
return list
|
|
}
|
|
|
|
// ========== Plant Detail 辅助函数 ==========
|
|
|
|
func queryImagesByPlant(svcCtx *svc.ServiceContext, ctx context.Context, plantID string) []map[string]interface{} {
|
|
type rel struct {
|
|
OssID string `gorm:"column:sundynix_oss_id"`
|
|
}
|
|
var rels []rel
|
|
svcCtx.DB.Table("sundynix_plant_my_plant_oss").
|
|
Where("sundynix_my_plant_id = ?", plantID).
|
|
Order("sundynix_oss_id asc").
|
|
Find(&rels)
|
|
var ids []string
|
|
for _, r := range rels {
|
|
ids = append(ids, r.OssID)
|
|
}
|
|
fileMap := fetchFileMap(svcCtx, ctx, ids)
|
|
var result []map[string]interface{}
|
|
for _, id := range ids {
|
|
if f, ok := fileMap[id]; ok {
|
|
result = append(result, f)
|
|
}
|
|
}
|
|
if result == nil {
|
|
result = []map[string]interface{}{}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func queryGrowthImages(svcCtx *svc.ServiceContext, ctx context.Context, records []*plantModel.GrowthRecord) map[string][]map[string]interface{} {
|
|
if len(records) == 0 {
|
|
return nil
|
|
}
|
|
var ids []string
|
|
for _, gr := range records {
|
|
ids = append(ids, gr.ID)
|
|
}
|
|
type rel struct {
|
|
GrowthRecordID string `gorm:"column:growth_record_id"`
|
|
OssID string `gorm:"column:oss_id"`
|
|
}
|
|
var rels []rel
|
|
svcCtx.DB.Table("sundynix_plant_growth_record_oss").
|
|
Where("growth_record_id IN ?", ids).
|
|
Find(&rels)
|
|
|
|
var allOssIds []string
|
|
gidMap := make(map[string][]string)
|
|
for _, r := range rels {
|
|
gidMap[r.GrowthRecordID] = append(gidMap[r.GrowthRecordID], r.OssID)
|
|
allOssIds = append(allOssIds, r.OssID)
|
|
}
|
|
fileMap := fetchFileMap(svcCtx, ctx, allOssIds)
|
|
|
|
result := make(map[string][]map[string]interface{})
|
|
for gid, ossIds := range gidMap {
|
|
var imgs []map[string]interface{}
|
|
for _, oid := range ossIds {
|
|
if f, ok := fileMap[oid]; ok {
|
|
imgs = append(imgs, f)
|
|
}
|
|
}
|
|
result[gid] = imgs
|
|
}
|
|
return result
|
|
}
|
|
|
|
func toPlantMap(p plantModel.MyPlant) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"id": p.ID, "createdAt": p.CreatedAt.Format(time.RFC3339),
|
|
"updatedAt": p.UpdatedAt.Format(time.RFC3339), "createdAtStr": p.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
"userId": p.UserID, "name": p.Name, "plantTime": p.PlantTime.Format(time.RFC3339),
|
|
"status": p.Status, "placement": p.Placement,
|
|
"potMaterial": p.PotMaterial, "potSize": p.PotSize,
|
|
"sunlight": p.Sunlight, "plantingMaterial": p.PlantingMaterial,
|
|
}
|
|
}
|
|
|
|
func toPlanMapList(plans []*plantModel.CarePlan) []map[string]interface{} {
|
|
var list []map[string]interface{}
|
|
for _, p := range plans {
|
|
list = append(list, map[string]interface{}{
|
|
"id": p.ID, "createdAt": p.CreatedAt.Format(time.RFC3339),
|
|
"updatedAt": p.UpdatedAt.Format(time.RFC3339), "createdAtStr": p.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
"userId": p.UserID, "plantId": p.PlantID, "name": p.Name, "icon": p.Icon,
|
|
"period": p.Period, "targetAction": p.TargetAction,
|
|
})
|
|
}
|
|
if list == nil {
|
|
list = []map[string]interface{}{}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func toRecordMapList(records []*plantModel.CareRecord) []map[string]interface{} {
|
|
var list []map[string]interface{}
|
|
for _, r := range records {
|
|
list = append(list, map[string]interface{}{
|
|
"id": r.ID, "createdAt": r.CreatedAt.Format(time.RFC3339),
|
|
"updatedAt": r.UpdatedAt.Format(time.RFC3339), "createdAtStr": r.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
"userId": r.UserID, "plantId": r.PlantID, "planId": r.PlanID,
|
|
"name": r.Name, "remark": r.Remark, "icon": r.Icon,
|
|
})
|
|
}
|
|
if list == nil {
|
|
list = []map[string]interface{}{}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func toGrowthMapList(records []*plantModel.GrowthRecord, imgMap map[string][]map[string]interface{}) []map[string]interface{} {
|
|
var list []map[string]interface{}
|
|
for _, gr := range records {
|
|
imgs := imgMap[gr.ID]
|
|
if imgs == nil {
|
|
imgs = []map[string]interface{}{}
|
|
}
|
|
list = append(list, map[string]interface{}{
|
|
"id": gr.ID, "createdAt": gr.CreatedAt.Format(time.RFC3339),
|
|
"updatedAt": gr.UpdatedAt.Format(time.RFC3339), "createdAtStr": gr.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
"plantId": gr.PlantID, "userId": gr.UserID, "name": gr.Name,
|
|
"tag": gr.Tag, "desc": gr.Desc, "content": gr.Content,
|
|
"imgList": imgs,
|
|
})
|
|
}
|
|
if list == nil {
|
|
list = []map[string]interface{}{}
|
|
}
|
|
return list
|
|
}
|
|
|
|
// queryOssList 批量查询 Oss 图片信息(供 WikiDetailHandler 使用)
|
|
func queryOssList(svcCtx *svc.ServiceContext, ossIds []string) []map[string]interface{} {
|
|
if len(ossIds) == 0 {
|
|
return []map[string]interface{}{}
|
|
}
|
|
type OssRow struct {
|
|
ID string `gorm:"column:id"`
|
|
Name string `gorm:"column:name"`
|
|
Url string `gorm:"column:url"`
|
|
Tag string `gorm:"column:tag"`
|
|
Key string `gorm:"column:key"`
|
|
Suffix string `gorm:"column:suffix"`
|
|
CreatedAt time.Time `gorm:"column:created_at"`
|
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
|
}
|
|
var rows []OssRow
|
|
svcCtx.DB.Table("sundynix_oss").Where("id IN ?", ossIds).Find(&rows)
|
|
ossMap := make(map[string]OssRow)
|
|
for _, row := range rows {
|
|
ossMap[row.ID] = row
|
|
}
|
|
var result []map[string]interface{}
|
|
for _, id := range ossIds {
|
|
if row, ok := ossMap[id]; ok {
|
|
result = append(result, map[string]interface{}{
|
|
"id": row.ID, "name": row.Name, "url": row.Url, "tag": row.Tag,
|
|
"key": row.Key, "suffix": row.Suffix,
|
|
"createdAt": row.CreatedAt.Format(time.RFC3339),
|
|
"updatedAt": row.UpdatedAt.Format(time.RFC3339),
|
|
"createdAtStr": row.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
})
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func AiChatSyncHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
resp, err := svcCtx.PlantRpc.SyncAllWikiVector(r.Context(), &plantPb.PageReq{})
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.OkWithMsg(w, resp.Msg)
|
|
}
|
|
}
|
|
|
|
func WikiSyncQdrantHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
wikiID := idFromQuery(r)
|
|
if wikiID == "" {
|
|
var req types.IdReq
|
|
_ = httpx.Parse(r, &req)
|
|
wikiID = req.Id
|
|
}
|
|
if wikiID == "" {
|
|
response.Fail(w, "wikiId 不能为空")
|
|
return
|
|
}
|
|
if _, err := svcCtx.PlantRpc.SyncWikiVector(r.Context(), &plantPb.SyncWikiVectorReq{WikiId: wikiID}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func WikiDeleteQdrantHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
wikiID := idFromQuery(r)
|
|
if wikiID == "" {
|
|
var req types.IdReq
|
|
_ = httpx.Parse(r, &req)
|
|
wikiID = req.Id
|
|
}
|
|
if wikiID == "" {
|
|
response.Fail(w, "wikiId 不能为空")
|
|
return
|
|
}
|
|
if _, err := svcCtx.PlantRpc.DeleteWikiVector(r.Context(), &plantPb.SyncWikiVectorReq{WikiId: wikiID}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func WikiUploadImgHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
response.FailWithCode(w, 409, "设计/百科图片上传已迁移到 file-api,请先调用文件服务上传,再将返回的文件 id 作为 ossIds 传给 plant")
|
|
}
|
|
}
|
|
|
|
func BadgeConfigDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
resp, err := svcCtx.PlantRpc.GetBadgeConfigDetail(r.Context(), &plantPb.IdReq{Id: idFromQuery(r)})
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.OkWithData(w, resp)
|
|
}
|
|
}
|
|
|
|
func BadgeConfigDeleteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := svcCtx.PlantRpc.DeleteBadgeConfig(r.Context(), &plantPb.IdsReq{Ids: []string{idFromQuery(r)}}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func LevelConfigDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
resp, err := svcCtx.PlantRpc.GetLevelConfigDetail(r.Context(), &plantPb.IdReq{Id: idFromQuery(r)})
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.OkWithData(w, resp)
|
|
}
|
|
}
|
|
|
|
func MediaCheckCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var payload struct {
|
|
TraceID string `json:"traceId"`
|
|
PostID string `json:"postId"`
|
|
OssID string `json:"ossId"`
|
|
UserID string `json:"userId"`
|
|
Status int32 `json:"status"`
|
|
Type int32 `json:"type"`
|
|
ErrMsg string `json:"errMsg"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
|
|
if _, err := svcCtx.PlantRpc.MediaCheckCallback(r.Context(), &plantPb.MediaCheckCallbackReq{
|
|
TraceId: payload.TraceID,
|
|
PostId: payload.PostID,
|
|
OssId: payload.OssID,
|
|
UserId: payload.UserID,
|
|
Status: payload.Status,
|
|
Type: payload.Type,
|
|
ErrMsg: payload.ErrMsg,
|
|
}); err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
response.Ok(w)
|
|
}
|
|
}
|
|
|
|
func AiChatStreamHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query().Get("query")
|
|
if query == "" {
|
|
query = r.URL.Query().Get("question")
|
|
}
|
|
if query == "" {
|
|
response.Fail(w, "query 不能为空")
|
|
return
|
|
}
|
|
userId := fmt.Sprintf("%v", r.Context().Value("userId"))
|
|
|
|
header := w.Header()
|
|
header.Set("Content-Type", "text/event-stream")
|
|
header.Set("Cache-Control", "no-cache")
|
|
header.Set("Connection", "keep-alive")
|
|
header.Set("Transfer-Encoding", "chunked")
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
err := aiLogic.StreamChat(r.Context(), svcCtx, userId, query, w)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(w, "data: [ERROR] %v\n\n", err)
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
} else {
|
|
_, _ = fmt.Fprint(w, "data: [DONE]\n\n")
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getBaiduAccessToken(apiKey, secretKey string) (string, error) {
|
|
tokenURL := fmt.Sprintf(
|
|
"https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
|
|
apiKey, secretKey,
|
|
)
|
|
resp, err := http.Post(tokenURL, "application/x-www-form-urlencoded", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var tokenObj struct {
|
|
AccessToken string `json:"access_token"`
|
|
}
|
|
if err = json.Unmarshal(body, &tokenObj); err != nil || tokenObj.AccessToken == "" {
|
|
return "", fmt.Errorf("解析百度 token 失败")
|
|
}
|
|
return tokenObj.AccessToken, nil
|
|
}
|
|
|
|
func ClassifyPlantHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
contentType := r.Header.Get("Content-Type")
|
|
if !strings.Contains(contentType, "multipart/form-data") {
|
|
var req types.OcrReq
|
|
if err := httpx.Parse(r, &req); err != nil {
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
}
|
|
if req.ImageUrl == "" {
|
|
response.Fail(w, "接收文件失败: imageUrl 不能为空并且必须使用 multipart/form-data 上传文件")
|
|
return
|
|
}
|
|
l := ocrLogic.NewOcrClassifyLogic(r.Context(), svcCtx)
|
|
resp, err := l.OcrClassify(&req)
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
} else {
|
|
response.OkWithData(w, resp)
|
|
}
|
|
return
|
|
}
|
|
|
|
file, _, err := r.FormFile("file")
|
|
if err != nil {
|
|
response.Fail(w, "接收文件失败: "+err.Error())
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
fileBytes, err := io.ReadAll(file)
|
|
if err != nil {
|
|
response.Fail(w, "读取文件失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
apiKey := svcCtx.Config.BaiduOcr.ApiKey
|
|
secretKey := svcCtx.Config.BaiduOcr.SecretKey
|
|
if apiKey == "" || secretKey == "" {
|
|
response.Fail(w, "百度 OCR 未配置 ApiKey/SecretKey")
|
|
return
|
|
}
|
|
|
|
accessToken, err := getBaiduAccessToken(apiKey, secretKey)
|
|
if err != nil {
|
|
response.Fail(w, err.Error())
|
|
return
|
|
}
|
|
|
|
base64Str := base64.StdEncoding.EncodeToString(fileBytes)
|
|
escapedBase64 := url.QueryEscape(base64Str)
|
|
payload := strings.NewReader("image=" + escapedBase64 + "&baike_num=1")
|
|
|
|
apiURL := "https://aip.baidubce.com/rest/2.0/image-classify/v1/plant?access_token=" + accessToken
|
|
classifyReq, err := http.NewRequest("POST", apiURL, payload)
|
|
if err != nil {
|
|
response.Fail(w, "创建请求失败: "+err.Error())
|
|
return
|
|
}
|
|
classifyReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
client := &http.Client{}
|
|
classifyResp, err := client.Do(classifyReq)
|
|
if err != nil {
|
|
response.Fail(w, "调用百度植物识别接口失败: "+err.Error())
|
|
return
|
|
}
|
|
defer classifyResp.Body.Close()
|
|
|
|
body, err := io.ReadAll(classifyResp.Body)
|
|
if err != nil {
|
|
response.Fail(w, "读取识别结果失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
var baiduResp struct {
|
|
LogId uint64 `json:"log_id"`
|
|
Result []struct {
|
|
Score float64 `json:"score"`
|
|
Name string `json:"name"`
|
|
BaikeInfo *struct {
|
|
BaikeUrl string `json:"baike_url"`
|
|
ImageUrl string `json:"image_url"`
|
|
Description string `json:"description"`
|
|
} `json:"baike_info"`
|
|
} `json:"result"`
|
|
}
|
|
_ = json.Unmarshal(body, &baiduResp)
|
|
|
|
if baiduResp.LogId > 0 {
|
|
var dbResults plantModel.ResultsArray = make(plantModel.ResultsArray, 0, len(baiduResp.Result))
|
|
for _, item := range baiduResp.Result {
|
|
var baikeInfo *plantModel.BaikeInfo
|
|
if item.BaikeInfo != nil {
|
|
baikeInfo = &plantModel.BaikeInfo{
|
|
BaikeUrl: item.BaikeInfo.BaikeUrl,
|
|
ImageUrl: item.BaikeInfo.ImageUrl,
|
|
Description: item.BaikeInfo.Description,
|
|
}
|
|
}
|
|
dbResults = append(dbResults, plantModel.ResultItem{
|
|
Score: item.Score,
|
|
Name: item.Name,
|
|
BaikeInfo: baikeInfo,
|
|
})
|
|
}
|
|
|
|
userID := fmt.Sprintf("%v", r.Context().Value("userId"))
|
|
record := plantModel.ClassifyRecord{
|
|
UserID: userID,
|
|
LogID: baiduResp.LogId,
|
|
AllResults: dbResults,
|
|
}
|
|
if errDb := svcCtx.DB.Create(&record).Error; errDb != nil {
|
|
fmt.Printf("植物识别记录写入数据库失败: %v\n", errDb)
|
|
}
|
|
}
|
|
|
|
var result interface{}
|
|
if err = json.Unmarshal(body, &result); err != nil {
|
|
response.Fail(w, "解析识别结果失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
response.OkWithData(w, result)
|
|
}
|
|
}
|
|
|
|
// AddCarePlanHandler 兼容旧小程序的批量添加养护计划
|
|
func AddCarePlanHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
CarePlan []struct {
|
|
PlantID string `json:"plantId"`
|
|
Name string `json:"name"`
|
|
Period int32 `json:"period"`
|
|
Icon string `json:"icon"`
|
|
TargetAction string `json:"targetAction"`
|
|
} `json:"carePlan"`
|
|
}
|
|
|
|
if err := httpx.Parse(r, &req); err != nil {
|
|
response.Fail(w, "解析请求参数失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
userId := fmt.Sprintf("%v", r.Context().Value("userId"))
|
|
|
|
for _, item := range req.CarePlan {
|
|
if item.PlantID == "" {
|
|
response.Fail(w, "plantId 不能为空")
|
|
return
|
|
}
|
|
_, err := svcCtx.PlantRpc.AddCarePlan(r.Context(), &plantPb.AddCarePlanReq{
|
|
UserId: userId,
|
|
PlantId: item.PlantID,
|
|
Name: item.Name,
|
|
Icon: item.Icon,
|
|
Period: item.Period,
|
|
TargetAction: item.TargetAction,
|
|
})
|
|
if err != nil {
|
|
response.Fail(w, "添加养护计划失败: "+err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
response.Ok(w)
|
|
}
|
|
}
|