From dd6a638982a2a3af51af73888ca9480bde96a1d1 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Tue, 10 Feb 2026 17:14:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A4=8D=E7=89=A9=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/plant/ocr.go | 37 ++++++++++++++++++-- initialize/gorm.go | 21 +++++------ model/plant/my_classify_record.go | 56 +++++++++++++++++++++++++++++ router/plant/ocr_router.go | 5 +-- router/system/user_router.go | 2 +- service/plant/ocr.go | 58 ++++++++++++++++++++++++++++++- 6 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 model/plant/my_classify_record.go diff --git a/api/v1/plant/ocr.go b/api/v1/plant/ocr.go index 1de796a..f46510b 100644 --- a/api/v1/plant/ocr.go +++ b/api/v1/plant/ocr.go @@ -2,7 +2,9 @@ package plant import ( "sundynix-go/global" + "sundynix-go/model/commom/request" "sundynix-go/model/commom/response" + "sundynix-go/utils/auth" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -18,7 +20,7 @@ type OcrApi struct{} // @Produce application/json // @Param file formData file true "植物识别" // @Success 200 {object} response.Response{msg=string} "文件OCR" -// @router /ocr/base64 [post] +// @router /classify/plant [post] func (o *OcrApi) ClassifyPlant(c *gin.Context) { multipartFile, header, err := c.Request.FormFile("file") if err != nil { @@ -26,7 +28,8 @@ func (o *OcrApi) ClassifyPlant(c *gin.Context) { response.FailWithMsg("接收文件失败!", c) return } - res, err := ocrService.ClassifyPlant(multipartFile, header) + userId := auth.GetUserId(c) + res, err := ocrService.ClassifyPlant(multipartFile, header, userId) if err != nil { global.Logger.Error("植物识别识别!", zap.Error(err)) response.FailWithMsg("植物识别失败!", c) @@ -34,3 +37,33 @@ func (o *OcrApi) ClassifyPlant(c *gin.Context) { } response.OkWithData(res, c) } + +// MyClassifyLog +// @tags 识别相关 +// @Summary 我的植物识别记录 +// @Security ApiKeyAuth +// @accept json +// @Produce application/json +// @Param data body request.PageInfo true "分页" +// @Success 200 {object} response.Response{msg=string} "识别记录" +// @router /classify/myClassifyLog [post] +func (o *OcrApi) MyClassifyLog(c *gin.Context) { + var req request.PageInfo + if err := c.ShouldBind(&req); err != nil { + response.FailWithMsg("请求参数错误", c) + return + } + userId := auth.GetUserId(c) + list, total, err := ocrService.MyClassifyLog(req, userId) + if err != nil { + global.Logger.Error("获取识别记录失败!", zap.Error(err)) + response.FailWithMsg("获取识别记录失败!", c) + return + } + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: req.Current, + PageSize: req.PageSize, + }, c) +} diff --git a/initialize/gorm.go b/initialize/gorm.go index 0be1d39..1ef0000 100644 --- a/initialize/gorm.go +++ b/initialize/gorm.go @@ -39,16 +39,17 @@ func MigrateTable() { system.SysOperationRecord{}, system.Oss{}, - plant.MyPlant{}, //我的植物 - plant.CarePlan{}, //植物养护计划 - plant.CareTask{}, //植物养护任务 - plant.CareRecord{}, //植物养护记录 - plant.Topic{}, //帖子话题 - plant.Post{}, //帖子 - plant.PostLike{}, //帖子点赞 - plant.PostComment{}, //帖子评论 - plant.Class{}, //百科分类 - plant.Wiki{}, //百科植物 + plant.MyPlant{}, //我的植物 + plant.CarePlan{}, //植物养护计划 + plant.CareTask{}, //植物养护任务 + plant.CareRecord{}, //植物养护记录 + plant.Topic{}, //帖子话题 + plant.Post{}, //帖子 + plant.PostLike{}, //帖子点赞 + plant.PostComment{}, //帖子评论 + plant.Class{}, //百科分类 + plant.Wiki{}, //百科植物 + plant.ClassifyRecord{}, //植物识别记录 ) if err != nil { diff --git a/model/plant/my_classify_record.go b/model/plant/my_classify_record.go new file mode 100644 index 0000000..8a94050 --- /dev/null +++ b/model/plant/my_classify_record.go @@ -0,0 +1,56 @@ +package plant + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "sundynix-go/global" +) + +type ClassifyRecord struct { + global.BaseModel + UserId string `gorm:"uniqueIndex" json:"userId"` + LogId uint64 `gorm:"uniqueIndex" json:"logId"` + AllResults ResultsArray `gorm:"type:json" json:"allResults"` +} + +// BaikeInfo 植物百科信息(baike_info子对象) +type BaikeInfo struct { + BaikeUrl string `json:"baike_url"` // 百度百科链接 + ImageUrl string `json:"image_url"` // 植物图片链接 + Description string `json:"description"` // 植物百科描述文本 +} + +// ResultItem 识别结果项(result数组中的单个元素) +type ResultItem struct { + Score float64 `json:"score"` // 匹配相似度得分(0-1) + Name string `json:"name"` // 植物名称 + BaikeInfo *BaikeInfo `json:"baike_info"` // 植物百科信息(部分结果可能无此字段,用指针避免空值解析问题) +} + +type ResultsArray []ResultItem + +// Scan 实现 sql.Scanner 接口:JSON String -> Go Struct (读库) +// 3. 实现 sql.Scanner 接口 (读库) +// 必须是指针接收者,否则无法修改 r 的值 +func (r *ResultsArray) Scan(value interface{}) error { + if value == nil { + *r = make([]ResultItem, 0) + return nil + } + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + return json.Unmarshal(bytes, r) +} + +// Value 实现 driver.Valuer 接口:Go Struct -> JSON String (存库) +func (r ResultsArray) Value() (driver.Value, error) { + // 如果是 nil 或空数组,存为空 JSON 数组 + if len(r) == 0 { + return "[]", nil + } + // 序列化为 JSON 字符串 + return json.Marshal(r) +} diff --git a/router/plant/ocr_router.go b/router/plant/ocr_router.go index 218ca9a..6aaf644 100644 --- a/router/plant/ocr_router.go +++ b/router/plant/ocr_router.go @@ -5,9 +5,10 @@ import "github.com/gin-gonic/gin" type OcrRouter struct{} func (c *OcrRouter) InitOcrRouter(Router *gin.RouterGroup) { - badgeRouter := Router.Group("classify") + ocrRouter := Router.Group("classify") { - badgeRouter.POST("/plant", ocrApi.ClassifyPlant) + ocrRouter.POST("/plant", ocrApi.ClassifyPlant) + ocrRouter.POST("/myClassifyLog", ocrApi.MyClassifyLog) } } diff --git a/router/system/user_router.go b/router/system/user_router.go index adf1b9e..fc01c2e 100644 --- a/router/system/user_router.go +++ b/router/system/user_router.go @@ -10,7 +10,7 @@ type UserRouter struct { func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) { userRouter := Router.Group("user") { - userRouter.POST("info", userApi.CurrentUser) + userRouter.GET("info", userApi.CurrentUser) userRouter.POST("save", userApi.SaveUser) userRouter.POST("update", userApi.UpdateUser) userRouter.POST("getUserList", userApi.GetUserList) diff --git a/service/plant/ocr.go b/service/plant/ocr.go index 2774e0c..bc9d014 100644 --- a/service/plant/ocr.go +++ b/service/plant/ocr.go @@ -10,6 +10,8 @@ import ( "net/url" "strings" "sundynix-go/global" + "sundynix-go/model/commom/request" + "sundynix-go/model/plant" "sundynix-go/model/plant/response" "sundynix-go/pkg/httpclient" @@ -19,7 +21,7 @@ import ( type OcrService struct{} // ClassifyPlant 植物识别 -func (s *OcrService) ClassifyPlant(file multipart.File, header *multipart.FileHeader) (response.PlantRecognitionResponse, error) { +func (s *OcrService) ClassifyPlant(file multipart.File, header *multipart.FileHeader, userId string) (response.PlantRecognitionResponse, error) { reqUrl := "https://aip.baidubce.com/rest/2.0/image-classify/v1/plant?access_token=" + getAccessToken() // 3. 读取文件的全部字节 fileBytes, err := io.ReadAll(file) @@ -55,7 +57,61 @@ func (s *OcrService) ClassifyPlant(file multipart.File, header *multipart.FileHe if err = json.Unmarshal(body, &plantResp); err != nil { global.Logger.Error("解析识别JSON失败!", zap.Error(err)) } + + //4.异步写入库 + go func(userId string, apiResp response.PlantRecognitionResponse) { + // A. 安全防护:防止协程 Panic 导致程序崩溃 + defer func() { + if r := recover(); r != nil { + global.Logger.Error("异步入库发生 Panic", zap.Any("recover", r)) + } + }() + var dbResults plant.ResultsArray = make(plant.ResultsArray, 0, len(apiResp.Result)) + // 2. 循环搬运数据 + for _, item := range apiResp.Result { + // 处理嵌套的 BaikeInfo 指针 + var baikeInfo *plant.BaikeInfo + if item.BaikeInfo != nil { + baikeInfo = &plant.BaikeInfo{ + BaikeUrl: item.BaikeInfo.BaikeUrl, + ImageUrl: item.BaikeInfo.ImageUrl, + Description: item.BaikeInfo.Description, + } + } + // 添加转换后的对象 + dbResults = append(dbResults, plant.ResultItem{ + Score: item.Score, + Name: item.Name, + BaikeInfo: baikeInfo, // 赋值刚才处理好的指针 + }) + } + record := plant.ClassifyRecord{ + UserId: userId, + LogId: apiResp.LogId, + AllResults: dbResults, + } + if err := global.DB.Create(&record).Error; err != nil { + global.Logger.Error("异步植物识别结果入库失败", zap.Error(err)) + } + }(userId, plantResp) + // 立即返回结果 return plantResp, err + +} + +// MyClassifyLog 我的植物识别记录 +func (s *OcrService) MyClassifyLog(req request.PageInfo, id string) (list interface{}, total int64, err error) { + limit := req.PageSize + offset := req.PageSize * (req.Current - 1) + db := global.DB.Model(&plant.ClassifyRecord{}) + var records []plant.ClassifyRecord + err = db.Count(&total).Error + if err != nil { + return + } + db = db.Where("user_id = ?", id).Limit(limit).Offset(offset).Order("created_at desc").Find(&records) + return records, total, err + } func getAccessToken() string {