From 67968abcf37cff38456e7e4b286d9a4ae8c44dd2 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Mon, 24 Nov 2025 16:18:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E6=96=B0=E5=A2=9EVIID=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=A4=84=E7=90=86=E4=BA=BA=E8=84=B8=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现人脸数据结构定义与解析 - 添加系统注册、保活、注销代理逻辑 - 支持系统时间获取接口 - 处理人脸图片Base64解码及缩略图生成 - 异步执行人脸数据上传流程 - 提供成功与错误响应封装方法 --- api/viid_handler.go | 248 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 api/viid_handler.go diff --git a/api/viid_handler.go b/api/viid_handler.go new file mode 100644 index 0000000..96a7720 --- /dev/null +++ b/api/viid_handler.go @@ -0,0 +1,248 @@ +package api + +import ( + "ZhenTuLocalPassiveAdapter/config" + "ZhenTuLocalPassiveAdapter/logger" + "ZhenTuLocalPassiveAdapter/util" + "context" + "encoding/base64" + "io" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// Request Structs for Handler + +type FaceUploadRequest struct { + FaceListObject FaceListObject `json:"FaceListObject"` +} + +type FaceListObject struct { + FaceObject []FaceObject `json:"FaceObject"` +} + +type FaceObject struct { + FaceID string `json:"FaceID"` + DeviceID string `json:"DeviceID"` + ShotTime string `json:"ShotTime"` + FaceAppearTime string `json:"FaceAppearTime"` + SubImageList SubImageList `json:"SubImageList"` + // Position info + LeftTopX *int `json:"LeftTopX,omitempty"` + LeftTopY *int `json:"LeftTopY,omitempty"` + RightBtmX *int `json:"RightBtmX,omitempty"` + RightBtmY *int `json:"RightBtmY,omitempty"` +} + +type SubImageList struct { + SubImageInfoObject []SubImageInfoObject `json:"SubImageInfoObject"` +} + +type SubImageInfoObject struct { + ImageID string `json:"ImageID"` + Type string `json:"Type"` // "11"=Face, "14"=Source + FileFormat string `json:"FileFormat"` + Data string `json:"Data"` // Base64 + Width *int `json:"Width,omitempty"` + Height *int `json:"Height,omitempty"` +} + +// Register Routes +func RegisterVIIDRoutes(r *gin.Engine) { + viid := r.Group("/VIID") + { + sys := viid.Group("/System") + { + sys.POST("/Register", HandleRegister) + sys.POST("/Keepalive", HandleKeepalive) + sys.POST("/UnRegister", HandleUnRegister) + sys.GET("/Time", HandleSystemTime) + } + viid.POST("/Faces", HandleUploadFaces) + } +} + +// HTTP Handlers + +func HandleRegister(c *gin.Context) { + proxyHelper(c, "/VIID/System/Register") +} + +func HandleKeepalive(c *gin.Context) { + proxyHelper(c, "/VIID/System/Keepalive") +} + +func HandleUnRegister(c *gin.Context) { + proxyHelper(c, "/VIID/System/UnRegister") +} + +func proxyHelper(c *gin.Context, path string) { + resp, err := ProxyRequest(c.Request.Context(), c.Request.Method, path, c.Request.Body, c.Request.Header) + if err != nil { + logger.Error("Proxy request failed", zap.String("path", path), zap.Error(err)) + c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) + return + } + defer resp.Body.Close() + + // Copy Headers + for k, v := range resp.Header { + c.Writer.Header()[k] = v + } + c.Status(resp.StatusCode) + io.Copy(c.Writer, resp.Body) +} + +func HandleSystemTime(c *gin.Context) { + now := time.Now() + timeStr := now.Format("20060102150405") // VIID format + + // Determine TimeZone string (e.g., "Asia/Shanghai") + // Simple fixed string or system local + timeZone := "Local" + loc, err := time.LoadLocation("Local") + if err == nil { + timeZone = loc.String() + } + + resp := map[string]interface{}{ + "SystemTimeObject": map[string]string{ + "VIIDServerID": "00000000000000000001", // Placeholder + "TimeMode": "2", // 2: Atomic Clock / NTP + "LocalTime": timeStr, + "TimeZone": timeZone, + }, + } + + c.JSON(http.StatusOK, resp) +} + +func HandleUploadFaces(c *gin.Context) { + var req FaceUploadRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.Error("Invalid JSON in UploadFaces", zap.Error(err)) + sendErrorResponse(c, "/VIID/Faces", "", "Invalid JSON") + return + } + + var lastFaceID string + + for _, face := range req.FaceListObject.FaceObject { + lastFaceID = face.FaceID + deviceID := face.DeviceID + + // 1. Parse Images + var faceImg, srcImg []byte + var imgWidth, imgHeight int + + for _, subImg := range face.SubImageList.SubImageInfoObject { + data, err := base64.StdEncoding.DecodeString(subImg.Data) + if err != nil { + logger.Error("Base64 decode failed", zap.String("faceId", face.FaceID), zap.Error(err)) + continue + } + + if subImg.Type == "11" { // Face + faceImg = data + } else if subImg.Type == "14" { // Source + srcImg = data + if subImg.Width != nil { + imgWidth = *subImg.Width + } + if subImg.Height != nil { + imgHeight = *subImg.Height + } + } + } + + // 2. Construct Position Info + pos := FacePositionInfo{ + ImgWidth: imgWidth, + ImgHeight: imgHeight, + ShotTime: parseShotTime(face.ShotTime, face.FaceAppearTime), + } + if face.LeftTopX != nil { + pos.LeftTopX = *face.LeftTopX + } + if face.LeftTopY != nil { + pos.LeftTopY = *face.LeftTopY + } + if face.RightBtmX != nil { + pos.RightBtmX = *face.RightBtmX + } + if face.RightBtmY != nil { + pos.RightBtmY = *face.RightBtmY + } + + // 3. Generate Thumbnail (if Source Image and Coordinates exist) + var thumbImg []byte + if len(srcImg) > 0 && pos.LeftTopX > 0 && pos.LeftTopY > 0 { // Basic validation + // Use Source Image to generate thumbnail + // The thumbnail is 1/2 original size, centered on face + var err error + thumbImg, err = util.GenerateThumbnailFromCoords(srcImg, pos.LeftTopX, pos.LeftTopY, pos.RightBtmX, pos.RightBtmY) + if err != nil { + logger.Error("Failed to generate thumbnail", zap.String("faceId", face.FaceID), zap.Error(err)) + // Continue without thumbnail? or fail? + // Usually continue, but log error. + } + } + + // 4. Execute Upload Flow + scenicId := config.Config.Viid.ScenicId + + go func(fID, dID string, fData, tData, sData []byte, p FacePositionInfo) { + // Create a detached context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + if err := UploadFaceData(ctx, scenicId, dID, fData, tData, sData, p); err != nil { + logger.Error("Failed to process face upload", zap.String("faceId", fID), zap.Error(err)) + } + }(face.FaceID, deviceID, faceImg, thumbImg, srcImg, pos) + } + + // Respond Success + sendSuccessResponse(c, "/VIID/Faces", lastFaceID, "OK") +} + +// Helpers + +func sendErrorResponse(c *gin.Context, url, id, msg string) { + resp := VIIDBaseResponse{ + ResponseStatusObject: ResponseStatusObject{ + ID: id, + RequestURL: url, + StatusCode: "1", + StatusString: msg, + LocalTime: time.Now().Format("20060102150405"), + }, + } + c.JSON(http.StatusBadRequest, resp) +} + +func sendSuccessResponse(c *gin.Context, url, id, msg string) { + resp := VIIDBaseResponse{ + ResponseStatusObject: ResponseStatusObject{ + ID: id, + RequestURL: url, + StatusCode: "0", + StatusString: msg, + LocalTime: time.Now().Format("20060102150405"), + }, + } + c.JSON(http.StatusOK, resp) +} + +func parseShotTime(t1, t2 string) string { + if t1 != "" { + return t1 + } + if t2 != "" { + return t2 + } + return time.Now().Format("20060102150405") +}