Files
VptPassiveAdapter/api/viid_handler.go
Jerry Yan 67968abcf3 feat(api): 新增VIID接口处理人脸上传功能
- 实现人脸数据结构定义与解析
- 添加系统注册、保活、注销代理逻辑
- 支持系统时间获取接口
- 处理人脸图片Base64解码及缩略图生成
- 异步执行人脸数据上传流程
- 提供成功与错误响应封装方法
2025-11-24 16:18:09 +08:00

249 lines
6.3 KiB
Go

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")
}