feat: 植物识别记录
This commit is contained in:
+35
-2
@@ -2,7 +2,9 @@ package plant
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sundynix-go/global"
|
"sundynix-go/global"
|
||||||
|
"sundynix-go/model/commom/request"
|
||||||
"sundynix-go/model/commom/response"
|
"sundynix-go/model/commom/response"
|
||||||
|
"sundynix-go/utils/auth"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -18,7 +20,7 @@ type OcrApi struct{}
|
|||||||
// @Produce application/json
|
// @Produce application/json
|
||||||
// @Param file formData file true "植物识别"
|
// @Param file formData file true "植物识别"
|
||||||
// @Success 200 {object} response.Response{msg=string} "文件OCR"
|
// @Success 200 {object} response.Response{msg=string} "文件OCR"
|
||||||
// @router /ocr/base64 [post]
|
// @router /classify/plant [post]
|
||||||
func (o *OcrApi) ClassifyPlant(c *gin.Context) {
|
func (o *OcrApi) ClassifyPlant(c *gin.Context) {
|
||||||
multipartFile, header, err := c.Request.FormFile("file")
|
multipartFile, header, err := c.Request.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -26,7 +28,8 @@ func (o *OcrApi) ClassifyPlant(c *gin.Context) {
|
|||||||
response.FailWithMsg("接收文件失败!", c)
|
response.FailWithMsg("接收文件失败!", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := ocrService.ClassifyPlant(multipartFile, header)
|
userId := auth.GetUserId(c)
|
||||||
|
res, err := ocrService.ClassifyPlant(multipartFile, header, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.Logger.Error("植物识别识别!", zap.Error(err))
|
global.Logger.Error("植物识别识别!", zap.Error(err))
|
||||||
response.FailWithMsg("植物识别失败!", c)
|
response.FailWithMsg("植物识别失败!", c)
|
||||||
@@ -34,3 +37,33 @@ func (o *OcrApi) ClassifyPlant(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.OkWithData(res, c)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ func MigrateTable() {
|
|||||||
plant.PostComment{}, //帖子评论
|
plant.PostComment{}, //帖子评论
|
||||||
plant.Class{}, //百科分类
|
plant.Class{}, //百科分类
|
||||||
plant.Wiki{}, //百科植物
|
plant.Wiki{}, //百科植物
|
||||||
|
plant.ClassifyRecord{}, //植物识别记录
|
||||||
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ import "github.com/gin-gonic/gin"
|
|||||||
type OcrRouter struct{}
|
type OcrRouter struct{}
|
||||||
|
|
||||||
func (c *OcrRouter) InitOcrRouter(Router *gin.RouterGroup) {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type UserRouter struct {
|
|||||||
func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) {
|
func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) {
|
||||||
userRouter := Router.Group("user")
|
userRouter := Router.Group("user")
|
||||||
{
|
{
|
||||||
userRouter.POST("info", userApi.CurrentUser)
|
userRouter.GET("info", userApi.CurrentUser)
|
||||||
userRouter.POST("save", userApi.SaveUser)
|
userRouter.POST("save", userApi.SaveUser)
|
||||||
userRouter.POST("update", userApi.UpdateUser)
|
userRouter.POST("update", userApi.UpdateUser)
|
||||||
userRouter.POST("getUserList", userApi.GetUserList)
|
userRouter.POST("getUserList", userApi.GetUserList)
|
||||||
|
|||||||
+57
-1
@@ -10,6 +10,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sundynix-go/global"
|
"sundynix-go/global"
|
||||||
|
"sundynix-go/model/commom/request"
|
||||||
|
"sundynix-go/model/plant"
|
||||||
"sundynix-go/model/plant/response"
|
"sundynix-go/model/plant/response"
|
||||||
"sundynix-go/pkg/httpclient"
|
"sundynix-go/pkg/httpclient"
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ import (
|
|||||||
type OcrService struct{}
|
type OcrService struct{}
|
||||||
|
|
||||||
// ClassifyPlant 植物识别
|
// 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()
|
reqUrl := "https://aip.baidubce.com/rest/2.0/image-classify/v1/plant?access_token=" + getAccessToken()
|
||||||
// 3. 读取文件的全部字节
|
// 3. 读取文件的全部字节
|
||||||
fileBytes, err := io.ReadAll(file)
|
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 {
|
if err = json.Unmarshal(body, &plantResp); err != nil {
|
||||||
global.Logger.Error("解析识别JSON失败!", zap.Error(err))
|
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
|
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 {
|
func getAccessToken() string {
|
||||||
|
|||||||
Reference in New Issue
Block a user