feat: 植物识别百科ai助手迁移

This commit is contained in:
Blizzard
2026-05-24 01:41:22 +08:00
parent ae6d03d351
commit 076ed1509b
29 changed files with 1121 additions and 372 deletions
@@ -2,14 +2,20 @@ 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"
@@ -892,3 +898,227 @@ func MediaCheckCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
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)
}
}