Files
lone-services/services/analysis/internal/callback/faceback.go
T
zzw fd6d8a2300
CI / changes (push) Successful in 1m56s
CI / ad (push) Successful in 1s
CI / admin (push) Successful in 0s
CI / analysis (push) Successful in 37s
CI / bff (push) Successful in 0s
CI / chore (push) Successful in 0s
CI / equipment (push) Successful in 0s
CI / express (push) Successful in 0s
CI / factory (push) Successful in 0s
CI / message (push) Successful in 1s
CI / order (push) Successful in 0s
CI / payment (push) Successful in 1s
CI / percutaneous (push) Successful in 0s
CI / product (push) Successful in 1s
CI / record (push) Successful in 0s
CI / sale (push) Successful in 0s
CI / store (push) Successful in 0s
CI / support (push) Successful in 0s
CI / task (push) Successful in 1s
CI / user (push) Successful in 0s
CI / water (push) Successful in 1s
CI / wecom (push) Successful in 0s
CI / ws (push) Successful in 1s
chore: faceback create images
2026-10-27 15:20:38 +08:00

732 lines
19 KiB
Go

package callback
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"lone-services/pkg/modelbase"
pkgoss "lone-services/pkg/oss"
"lone-services/pkg/utils"
"lone-services/pkg/wechat/qrcode"
analysisconfig "lone-services/services/analysis/internal/config"
"lone-services/services/analysis/internal/dao"
"lone-services/services/analysis/internal/model"
"github.com/zeromicro/go-zero/core/logx"
)
type ImageRule struct {
Scene string
Position string
}
type FrontItem struct {
Name string `json:"name"`
Path string `json:"path"`
}
type AnalysisSummary struct {
HealthScore string `json:"health_score"`
NormalArea string `json:"normal_area"`
MildAbnormality string `json:"mild_abnormality"`
ModerateAbnormality string `json:"moderate_abnormality"`
SevereAbnormality string `json:"severe_abnormality"`
PredictedAge string `json:"predicted_age"`
}
func ProcessPyCallback(subTaskId string) error {
sub, err := GetSubTask(subTaskId)
if err != nil {
return err
}
if err = FinishSubTask(subTaskId); err != nil {
return err
}
subCopy := *sub
go func(s dao.ImageAnalysisSub) {
defer func() {
if r := recover(); r != nil {
logx.Errorf("py callback async panic: %v", r)
}
}()
if ossErr := UploadSubImagesToOSS(&s); ossErr != nil {
logx.Errorf("upload sub images to oss failed: %v", ossErr)
}
if finishErr := TryFinishTask(s.TaskId); finishErr != nil {
logx.Errorf("try finish task failed: %v", finishErr)
}
}(subCopy)
return nil
}
func GetSubTask(subTaskId string) (*dao.ImageAnalysisSub, error) {
var data dao.ImageAnalysisSub
err := model.ImageAnalysisSubModel{}.Init().GetOne(modelbase.Params{
Eq: map[string]string{"sub_task_id": subTaskId},
}, &data)
if err != nil {
return nil, err
}
if data.Id < utils.NumberOne {
return nil, fmt.Errorf("sub task not found: %s", subTaskId)
}
return &data, nil
}
func FinishSubTask(subTaskId string) error {
_, err := model.ImageAnalysisSubModel{}.Init().Edit(modelbase.Params{
Eq: map[string]string{"sub_task_id": subTaskId},
}, map[string]interface{}{
"status": dao.AnalysisStatusDone,
"process_end_ts": time.Now().UnixMilli(),
"updated_at": utils.Now(),
})
return err
}
func UploadSubImagesToOSS(sub *dao.ImageAnalysisSub) error {
if sub == nil || sub.SubTaskId == utils.StringEmpty {
return fmt.Errorf("sub task empty")
}
imageDir := sub.ImageDir
if imageDir == utils.StringEmpty {
imageDir = subImageDir(sub.FilePath)
}
if imageDir == utils.StringEmpty {
return fmt.Errorf("sub image dir empty")
}
scene, originalPaths, err := detectOriginalLighting(imageDir)
if err != nil {
return err
}
needResult := sceneHasResult(scene)
originalsReady := sub.LeftPath != utils.StringEmpty && sub.MidPath != utils.StringEmpty && sub.RightPath != utils.StringEmpty
resultsReady := !needResult || (sub.LeftResultPath != utils.StringEmpty && sub.MidResultPath != utils.StringEmpty && sub.RightResultPath != utils.StringEmpty)
if originalsReady && resultsReady {
return nil
}
update := map[string]interface{}{
"scene": scene,
"updated_at": utils.Now(),
}
if !originalsReady {
leftURL, midURL, rightURL, upErr := uploadPositionImages(sub.TaskId, originalPaths, true)
if upErr != nil {
return fmt.Errorf("upload original images: %w", upErr)
}
update["left_path"] = leftURL
update["mid_path"] = midURL
update["right_path"] = rightURL
sub.LeftPath = leftURL
sub.MidPath = midURL
sub.RightPath = rightURL
}
if needResult && !resultsReady {
analysis, aErr := GetAnalysis(sub.TaskId)
if aErr != nil {
return aErr
}
if analysis.FolderPath == utils.StringEmpty {
return fmt.Errorf("analysis folder_path empty task_id=%s", sub.TaskId)
}
resultPaths, pErr := resultImagePaths(analysis.FolderPath, scene)
if pErr != nil {
return pErr
}
leftURL, midURL, rightURL, upErr := uploadPositionImages(sub.TaskId, resultPaths, false)
if upErr != nil {
return fmt.Errorf("upload result images: %w", upErr)
}
update["left_result_path"] = leftURL
update["mid_result_path"] = midURL
update["right_result_path"] = rightURL
}
_, err = model.ImageAnalysisSubModel{}.Init().Edit(modelbase.Params{
Eq: map[string]string{"sub_task_id": sub.SubTaskId},
}, update)
return err
}
func sceneHasResult(scene string) bool {
switch scene {
case "uv405", "cross", "parallel", "uv365":
return true
default:
return false
}
}
func detectOriginalLighting(imageDir string) (string, map[string]string, error) {
entries, err := os.ReadDir(imageDir)
if err != nil {
return utils.StringEmpty, nil, err
}
scene := utils.StringEmpty
paths := map[string]string{}
for _, entry := range entries {
if entry.IsDir() {
continue
}
rule, ok := parseImageRule(entry.Name())
if !ok || !strings.HasPrefix(rule.Scene, "original_") {
continue
}
light := strings.TrimPrefix(rule.Scene, "original_")
if scene == utils.StringEmpty {
scene = light
}
if light != scene {
continue
}
paths[rule.Position] = filepath.Join(imageDir, entry.Name())
}
if scene == utils.StringEmpty || paths["left"] == "" || paths["mid"] == "" || paths["right"] == "" {
return utils.StringEmpty, nil, fmt.Errorf("no complete original lighting in %s", imageDir)
}
return scene, paths, nil
}
func resultImagePaths(folderPath, scene string) (map[string]string, error) {
var dir string
var names map[string]string
switch scene {
case "uv405":
dir = "output/yolo"
names = map[string]string{
"left": "yolo_uv405_left.jpg",
"mid": "yolo_uv405_mid.jpg",
"right": "yolo_uv405_right.jpg",
}
case "cross":
dir = "output/pigment"
names = map[string]string{
"left": "pigment_cross_left.jpg",
"mid": "pigment_cross_mid.jpg",
"right": "pigment_cross_right.jpg",
}
case "parallel":
dir = "output/sensitivity"
names = map[string]string{
"left": "sensitivity_parallel_left.jpg",
"mid": "sensitivity_parallel_mid.jpg",
"right": "sensitivity_parallel_right.jpg",
}
case "uv365":
dir = "output/redzone"
names = map[string]string{
"left": "red_uv365_left.jpg",
"mid": "red_uv365_mid.jpg",
"right": "red_uv365_right.jpg",
}
default:
return nil, fmt.Errorf("scene %s has no result images", scene)
}
paths := map[string]string{}
for pos, name := range names {
full := filepath.Join(folderPath, dir, name)
if _, err := os.Stat(full); err != nil {
return nil, fmt.Errorf("result image missing: %s", full)
}
paths[pos] = full
}
return paths, nil
}
func uploadPositionImages(taskId string, paths map[string]string, rotate bool) (string, string, string, error) {
leftURL, err := uploadImageToOSS(taskId, paths["left"], rotate)
if err != nil {
return utils.StringEmpty, utils.StringEmpty, utils.StringEmpty, fmt.Errorf("left: %w", err)
}
midURL, err := uploadImageToOSS(taskId, paths["mid"], rotate)
if err != nil {
return utils.StringEmpty, utils.StringEmpty, utils.StringEmpty, fmt.Errorf("mid: %w", err)
}
rightURL, err := uploadImageToOSS(taskId, paths["right"], rotate)
if err != nil {
return utils.StringEmpty, utils.StringEmpty, utils.StringEmpty, fmt.Errorf("right: %w", err)
}
return leftURL, midURL, rightURL, nil
}
func subImageDir(filePath string) string {
if filePath == utils.StringEmpty {
return utils.StringEmpty
}
if info, err := os.Stat(filePath); err == nil && info.IsDir() {
return filePath
}
ext := filepath.Ext(filePath)
if strings.EqualFold(ext, ".zip") {
return strings.TrimSuffix(filePath, ext)
}
return filepath.Dir(filePath)
}
func uploadImageToOSS(taskId, localPath string, rotate bool) (string, error) {
_ = taskId
data, err := os.ReadFile(localPath)
if err != nil {
return utils.StringEmpty, err
}
if rotate {
data, err = Rotate90Bytes(data, filepath.Ext(localPath))
if err != nil {
return utils.StringEmpty, fmt.Errorf("rotate image: %w", err)
}
}
ext := strings.ToLower(filepath.Ext(localPath))
if ext == utils.StringEmpty {
ext = ".jpg"
}
fileName := fmt.Sprintf("%d%d%s", time.Now().UnixNano(), rand.Intn(10000), ext)
contentType := "image/jpeg"
if ext == ".png" {
contentType = "image/png"
}
imageDir := utils.GetConfigString("oss.imageDir")
res, err := pkgoss.Upload(pkgoss.UploadInput{
Filename: fileName,
Content: data,
ContentType: contentType,
Dir: imageDir,
})
if err != nil {
return utils.StringEmpty, err
}
return res.URL, nil
}
func ListFinishedSubs(taskId string) ([]dao.ImageAnalysisSub, error) {
var list []dao.ImageAnalysisSub
err := model.ImageAnalysisSubModel{}.Init().Items(modelbase.Params{
Eq: map[string]string{
"task_id": taskId,
"status": strconv.Itoa(int(dao.AnalysisStatusDone)),
},
Order: "id asc",
}, &list)
return list, err
}
func TryFinishTask(taskId string) error {
count, err := CountFinishedTask(taskId)
if err != nil || count < 6 {
return err
}
subs, err := ListFinishedSubs(taskId)
if err != nil {
return err
}
if len(subs) < 6 {
return nil
}
for _, sub := range subs {
if sub.LeftPath == utils.StringEmpty || sub.MidPath == utils.StringEmpty || sub.RightPath == utils.StringEmpty {
return nil
}
if sceneHasResult(sub.Scene) && (sub.LeftResultPath == utils.StringEmpty || sub.MidResultPath == utils.StringEmpty || sub.RightResultPath == utils.StringEmpty) {
return nil
}
}
analysis, err := GetAnalysis(taskId)
if err != nil {
return err
}
if analysis.Status == dao.AnalysisStatusDone {
return nil
}
summary, err := ParseSummary(analysis.FolderPath)
if err != nil {
logx.Errorf("read summary failed: %v", err)
}
analysis.Cover = UploadCoverToOSS(taskId, analysis.Cover)
if err = FinishAnalysis(taskId, analysis); err != nil {
return err
}
userImage, err := FinishUserImage(analysis, summary)
if err != nil {
return err
}
if qrErr := EnsureReportQrcode(taskId, userImage.Id); qrErr != nil {
logx.Errorf("ensure report qrcode failed: %v", qrErr)
}
return nil
}
func CountFinishedTask(taskId string) (int64, error) {
return model.ImageAnalysisSubModel{}.Init().CountNumber(modelbase.Params{
Eq: map[string]string{
"task_id": taskId,
"status": strconv.Itoa(int(dao.AnalysisStatusDone)),
},
})
}
func EnsureReportQrcode(taskId string, imageId int64) error {
if taskId == utils.StringEmpty {
return fmt.Errorf("task_id empty")
}
if imageId <= 0 {
return fmt.Errorf("image id invalid")
}
analysis, err := GetAnalysis(taskId)
if err != nil {
return err
}
if analysis.Qrcode != utils.StringEmpty {
return nil
}
wxCfg, err := analysisconfig.LoadWechatConfig()
if err != nil {
return err
}
img, err := wxCfg.NewQrcodeClient().CreateWxaCode(context.Background(), &qrcode.WxaCodeRequest{
Path: qrcode.BuildPath(wxCfg.ReportPage, map[string]string{
"type": strconv.Itoa(utils.NumberOne),
"ids": strconv.FormatInt(imageId, utils.NumberTen),
}),
Width: wxCfg.Width,
EnvVersion: wxCfg.EnvVersion,
AutoColor: false,
LineColor: &qrcode.LineColor{R: 255, G: 255, B: 255},
IsHyaline: true,
})
if err != nil {
return fmt.Errorf("fetch mini qrcode: %w", err)
}
imageDir := utils.GetConfigString("oss.imageDir")
fileName := fmt.Sprintf("%d%d.png", time.Now().UnixNano(), rand.Intn(10000))
res, err := pkgoss.Upload(pkgoss.UploadInput{
Filename: fileName,
Content: img,
ContentType: "image/png",
Dir: imageDir,
})
if err != nil {
return err
}
affected, err := model.ImageAnalysisModel{}.Init().Edit(modelbase.Params{
Eq: map[string]string{
"task_id": taskId,
"qrcode": "",
},
}, map[string]interface{}{
"qrcode": res.URL,
"updated_at": utils.Now(),
})
if err != nil {
return fmt.Errorf("save qrcode: %w", err)
}
if affected == 0 {
logx.Infof("report qrcode already set by concurrent callback task_id=%s", taskId)
return nil
}
logx.Infof("report qrcode saved task_id=%s image_id=%d qrcode=%s", taskId, imageId, res.URL)
return nil
}
func GetAnalysis(taskId string) (*dao.ImageAnalysis, error) {
var task dao.ImageAnalysis
err := model.ImageAnalysisModel{}.Init().GetOne(modelbase.Params{
Eq: map[string]string{"task_id": taskId},
}, &task)
if err != nil {
return nil, err
}
if task.Id < utils.NumberOne {
return nil, fmt.Errorf("analysis not found: %s", taskId)
}
return &task, nil
}
func ParseSummary(folderPath string) (*AnalysisSummary, error) {
txtPath := path.Join(folderPath, "output/texts", "analysis_report.txt")
data, err := os.ReadFile(txtPath)
if err != nil {
return nil, err
}
result := &AnalysisSummary{}
items := strings.Split(string(data), ";;")
for _, item := range items {
item = strings.TrimSpace(item)
if item == utils.StringEmpty {
continue
}
parts := strings.Split(item, "::")
if len(parts) != utils.NumberTwo {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch key {
case "Health Score":
result.HealthScore = value
case "Normal area":
result.NormalArea = value
case "Mild abnormality":
result.MildAbnormality = value
case "Moderate abnormality":
result.ModerateAbnormality = value
case "Severe abnormality":
result.SevereAbnormality = value
case "Predicted Age":
result.PredictedAge = value
}
}
return result, nil
}
func FinishAnalysis(taskID string, analysis *dao.ImageAnalysis) error {
now := utils.Now()
_, err := model.ImageAnalysisModel{}.Init().Edit(modelbase.Params{
Eq: map[string]string{"task_id": taskID},
}, map[string]interface{}{
"status": dao.AnalysisStatusDone,
"cover": analysis.Cover,
"updated_at": now,
})
if err != nil {
return err
}
wLog := modelbase.Params{
Eq: map[string]string{
"user_id": strconv.FormatInt(analysis.UserId, utils.NumberTen),
},
}
var logRow dao.UserImageLog
err = model.UserImageLogModel{}.Init().GetOne(wLog, &logRow)
if err != nil {
return err
}
if logRow.Id < utils.NumberOne {
logModel := model.UserImageLogModel{}.Init()
return logModel.Create(&dao.UserImageLog{
UserId: analysis.UserId,
DetectionNumber: utils.NumberOne,
Cover: analysis.Cover,
DetectionTime: now,
Status: uint8(utils.StatusOk),
})
}
newNumber := logRow.DetectionNumber + utils.NumberOne
updateData := map[string]interface{}{
"cover": analysis.Cover,
"detection_time": now,
"detection_number": newNumber,
}
if newNumber == utils.NumberTwo {
updateData["status"] = utils.NumberTwo
}
_, err = model.UserImageLogModel{}.Init().Edit(wLog, updateData)
return err
}
func FinishUserImage(analysis *dao.ImageAnalysis, summaryStruct *AnalysisSummary) (*dao.Image, error) {
subs, err := ListFinishedSubs(analysis.TaskId)
if err != nil {
return nil, err
}
if len(subs) == 0 {
return nil, errors.New("no finished sub tasks")
}
userImage, err := BuildUserImageFromSubs(analysis, summaryStruct, subs)
if err != nil {
return nil, err
}
imgModel := model.ImageModel{}.Init()
if err = imgModel.Create(userImage); err != nil {
return nil, err
}
return userImage, nil
}
func BuildUserImageFromSubs(analysis *dao.ImageAnalysis, summaryStruct *AnalysisSummary, subs []dao.ImageAnalysisSub) (*dao.Image, error) {
summaryBytes, err := json.Marshal(summaryStruct)
if err != nil {
return nil, err
}
images := make(map[string]map[string]string)
var front []FrontItem
for _, sub := range subs {
if sub.Scene == utils.StringEmpty {
continue
}
if sub.LeftPath == utils.StringEmpty || sub.MidPath == utils.StringEmpty || sub.RightPath == utils.StringEmpty {
continue
}
originalScene := "original_" + sub.Scene
images[originalScene] = map[string]string{
"left": sub.LeftPath,
"mid": sub.MidPath,
"right": sub.RightPath,
}
front = append(front, FrontItem{
Name: originalScene,
Path: sub.MidPath,
})
if sub.LeftResultPath == utils.StringEmpty || sub.MidResultPath == utils.StringEmpty || sub.RightResultPath == utils.StringEmpty {
continue
}
images[sub.Scene] = map[string]string{
"left": sub.LeftResultPath,
"mid": sub.MidResultPath,
"right": sub.RightResultPath,
}
front = append(front, FrontItem{
Name: sub.Scene,
Path: sub.MidResultPath,
})
}
if len(images) == 0 {
return nil, errors.New("no oss images on finished sub tasks")
}
result := map[string]interface{}{
"images": images,
"front": front,
}
imagesBytes, err := json.Marshal(result)
if err != nil {
return nil, err
}
return &dao.Image{
UserId: analysis.UserId,
TaskId: analysis.TaskId,
Images: string(imagesBytes),
Contents: string(summaryBytes),
Cover: analysis.Cover,
Status: int8(utils.StatusOk),
CreateTime: utils.Now(),
}, nil
}
func UploadCoverToOSS(taskId, localPath string) string {
if localPath == utils.StringEmpty {
return utils.StringEmpty
}
if strings.HasPrefix(localPath, "http://") || strings.HasPrefix(localPath, "https://") {
return localPath
}
candidates := []string{localPath}
if !filepath.IsAbs(localPath) {
candidates = append(candidates, filepath.Join(utils.GetConfigString("file.upload_path"), localPath))
}
for _, fullPath := range candidates {
if _, err := os.Stat(fullPath); err != nil {
continue
}
url, err := uploadImageToOSS(taskId, fullPath, true)
if err != nil {
logx.Errorf("upload cover to oss failed: %v", err)
return localPath
}
return url
}
logx.Errorf("upload cover skipped, file not found: %s", localPath)
return localPath
}
func parseImageRule(fileName string) (ImageRule, bool) {
switch fileName {
case "yolo_uv405_left.jpg":
return ImageRule{"uv405", "left"}, true
case "yolo_uv405_mid.jpg":
return ImageRule{"uv405", "mid"}, true
case "yolo_uv405_right.jpg":
return ImageRule{"uv405", "right"}, true
case "pigment_cross_left.jpg":
return ImageRule{"cross", "left"}, true
case "pigment_cross_mid.jpg":
return ImageRule{"cross", "mid"}, true
case "pigment_cross_right.jpg":
return ImageRule{"cross", "right"}, true
case "sensitivity_cross_left.jpg":
return ImageRule{"parallel", "left"}, true
case "sensitivity_cross_mid.jpg":
return ImageRule{"parallel", "mid"}, true
case "sensitivity_cross_right.jpg":
return ImageRule{"parallel", "right"}, true
case "red_uv365_left.jpg":
return ImageRule{"uv365", "left"}, true
case "red_uv365_mid.jpg":
return ImageRule{"uv365", "mid"}, true
case "red_uv365_right.jpg":
return ImageRule{"uv365", "right"}, true
}
if strings.HasPrefix(fileName, "parallel_") {
return buildOriginalRule(fileName, "original_parallel")
}
if strings.HasPrefix(fileName, "cross_") {
return buildOriginalRule(fileName, "original_cross")
}
if strings.HasPrefix(fileName, "visible_") {
return buildOriginalRule(fileName, "original_visible")
}
if strings.HasPrefix(fileName, "uv365_") {
return buildOriginalRule(fileName, "original_uv365")
}
if strings.HasPrefix(fileName, "uv385_") {
return buildOriginalRule(fileName, "original_uv385")
}
if strings.HasPrefix(fileName, "uv405_") {
return buildOriginalRule(fileName, "original_uv405")
}
return ImageRule{}, false
}
func buildOriginalRule(fileName string, scene string) (ImageRule, bool) {
switch {
case strings.HasSuffix(fileName, "_left.jpg"):
return ImageRule{Scene: scene, Position: "left"}, true
case strings.HasSuffix(fileName, "_mid.jpg"):
return ImageRule{Scene: scene, Position: "mid"}, true
case strings.HasSuffix(fileName, "_right.jpg"):
return ImageRule{Scene: scene, Position: "right"}, true
}
return ImageRule{}, false
}