feat: 植物识别百科ai助手迁移
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user