feat: user improve
CI / changes (push) Successful in 1m31s
CI / ad (push) Successful in 32s
CI / admin (push) Successful in 26s
CI / analysis (push) Successful in 28s
CI / bff (push) Successful in 23s
CI / chore (push) Successful in 24s
CI / equipment (push) Successful in 21s
CI / express (push) Successful in 21s
CI / factory (push) Successful in 18s
CI / message (push) Successful in 18s
CI / order (push) Successful in 21s
CI / payment (push) Successful in 20s
CI / percutaneous (push) Successful in 19s
CI / product (push) Successful in 21s
CI / record (push) Successful in 21s
CI / sale (push) Successful in 20s
CI / store (push) Successful in 25s
CI / support (push) Successful in 21s
CI / task (push) Successful in 20s
CI / user (push) Successful in 22s
CI / water (push) Successful in 20s
CI / wecom (push) Successful in 21s
CI / ws (push) Successful in 8s
CI / changes (push) Successful in 1m31s
CI / ad (push) Successful in 32s
CI / admin (push) Successful in 26s
CI / analysis (push) Successful in 28s
CI / bff (push) Successful in 23s
CI / chore (push) Successful in 24s
CI / equipment (push) Successful in 21s
CI / express (push) Successful in 21s
CI / factory (push) Successful in 18s
CI / message (push) Successful in 18s
CI / order (push) Successful in 21s
CI / payment (push) Successful in 20s
CI / percutaneous (push) Successful in 19s
CI / product (push) Successful in 21s
CI / record (push) Successful in 21s
CI / sale (push) Successful in 20s
CI / store (push) Successful in 25s
CI / support (push) Successful in 21s
CI / task (push) Successful in 20s
CI / user (push) Successful in 22s
CI / water (push) Successful in 20s
CI / wecom (push) Successful in 21s
CI / ws (push) Successful in 8s
This commit is contained in:
@@ -18,12 +18,14 @@ type (
|
||||
DevicePolicyReq = analysis.DevicePolicyReq
|
||||
OssCallbackReq = analysis.OssCallbackReq
|
||||
OssCallbackResp = analysis.OssCallbackResp
|
||||
PyCallbackReq = analysis.PyCallbackReq
|
||||
Response = analysis.Response
|
||||
|
||||
AnalysisDevice interface {
|
||||
Policy(ctx context.Context, in *DevicePolicyReq, opts ...grpc.CallOption) (*Response, error)
|
||||
Display(ctx context.Context, in *DeviceDisplayReq, opts ...grpc.CallOption) (*Response, error)
|
||||
OssCallback(ctx context.Context, in *OssCallbackReq, opts ...grpc.CallOption) (*OssCallbackResp, error)
|
||||
PyCallback(ctx context.Context, in *PyCallbackReq, opts ...grpc.CallOption) (*Response, error)
|
||||
}
|
||||
|
||||
defaultAnalysisDevice struct {
|
||||
@@ -51,3 +53,8 @@ func (m *defaultAnalysisDevice) OssCallback(ctx context.Context, in *OssCallback
|
||||
client := analysis.NewAnalysisDeviceClient(m.cli.Conn())
|
||||
return client.OssCallback(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (m *defaultAnalysisDevice) PyCallback(ctx context.Context, in *PyCallbackReq, opts ...grpc.CallOption) (*Response, error) {
|
||||
client := analysis.NewAnalysisDeviceClient(m.cli.Conn())
|
||||
return client.PyCallback(ctx, in, opts...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
// ProcessPyCallback 对应旧项目 POST /v1/analysis/callback
|
||||
// 入参 task_id 实际为 python 侧的 sub_task_id。
|
||||
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 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)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TryFinishTask 六组图都已上传 OSS 后做最终落库。可重复调用,已完成则直接返回。
|
||||
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 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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package callback
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
func Rotate90Bytes(data []byte, ext string) ([]byte, error) {
|
||||
img, err := imaging.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img = imaging.Rotate90(img)
|
||||
|
||||
format := imaging.JPEG
|
||||
if strings.EqualFold(ext, ".png") {
|
||||
format = imaging.PNG
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err = imaging.Encode(&buf, img, format); err != nil {
|
||||
return nil, fmt.Errorf("encode image: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -3,19 +3,25 @@ package dao
|
||||
import "lone-services/pkg/utils"
|
||||
|
||||
type ImageAnalysisSub struct {
|
||||
Id int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
TaskId string `gorm:"column:task_id" json:"task_id"`
|
||||
SubTaskId string `gorm:"column:sub_task_id" json:"sub_task_id"`
|
||||
Status int8 `gorm:"column:status" json:"status"`
|
||||
FilePath string `gorm:"column:file_path" json:"file_path"`
|
||||
ImageDir string `gorm:"column:image_dir" json:"image_dir"`
|
||||
Scene string `gorm:"column:scene" json:"scene"`
|
||||
UploadStartTs int64 `gorm:"column:upload_start_ts" json:"upload_start_ts"`
|
||||
UploadEndTs int64 `gorm:"column:upload_end_ts" json:"upload_end_ts"`
|
||||
ProcessStartTs int64 `gorm:"column:process_start_ts" json:"process_start_ts"`
|
||||
ProcessEndTs int64 `gorm:"column:process_end_ts" json:"process_end_ts"`
|
||||
CreatedAt utils.CustomTime `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt utils.CustomTime `gorm:"column:updated_at" json:"updated_at"`
|
||||
Id int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
TaskId string `gorm:"column:task_id" json:"task_id"`
|
||||
SubTaskId string `gorm:"column:sub_task_id" json:"sub_task_id"`
|
||||
Status int8 `gorm:"column:status" json:"status"`
|
||||
FilePath string `gorm:"column:file_path" json:"file_path"`
|
||||
ImageDir string `gorm:"column:image_dir" json:"image_dir"`
|
||||
Scene string `gorm:"column:scene" json:"scene"`
|
||||
LeftPath string `gorm:"column:left_path" json:"left_path"`
|
||||
MidPath string `gorm:"column:mid_path" json:"mid_path"`
|
||||
RightPath string `gorm:"column:right_path" json:"right_path"`
|
||||
LeftResultPath string `gorm:"column:left_result_path" json:"left_result_path"`
|
||||
MidResultPath string `gorm:"column:mid_result_path" json:"mid_result_path"`
|
||||
RightResultPath string `gorm:"column:right_result_path" json:"right_result_path"`
|
||||
UploadStartTs int64 `gorm:"column:upload_start_ts" json:"upload_start_ts"`
|
||||
UploadEndTs int64 `gorm:"column:upload_end_ts" json:"upload_end_ts"`
|
||||
ProcessStartTs int64 `gorm:"column:process_start_ts" json:"process_start_ts"`
|
||||
ProcessEndTs int64 `gorm:"column:process_end_ts" json:"process_end_ts"`
|
||||
CreatedAt utils.CustomTime `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt utils.CustomTime `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type HandleZipResource struct {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const ossCallbackPath = "/api/v1/oss/callback"
|
||||
const ossCallbackPath = "/api/v2/analysis/oss/callback"
|
||||
|
||||
type OssCallbackLogic struct {
|
||||
ctx context.Context
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package analysisdevicelogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"lone-services/pkg/utils"
|
||||
"lone-services/pkg/validate"
|
||||
analysis "lone-services/rpc/analysis/pb"
|
||||
"lone-services/services/analysis/internal/callback"
|
||||
"lone-services/services/analysis/internal/logic/common"
|
||||
"lone-services/services/analysis/internal/svc"
|
||||
"lone-services/services/analysis/validator"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type PyCallbackLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func NewPyCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PyCallbackLogic {
|
||||
return &PyCallbackLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *PyCallbackLogic) PyCallback(in *analysis.PyCallbackReq) (*analysis.Response, error) {
|
||||
var req validator.PyCallbackValidator
|
||||
if msg := validate.ValidateFromProto(in, &req); msg != utils.StringEmpty {
|
||||
return common.OutResponse(utils.ErrorParams, msg), nil
|
||||
}
|
||||
|
||||
if err := callback.ProcessPyCallback(req.TaskId); err != nil {
|
||||
l.Errorf("py callback failed: %v", err)
|
||||
return common.FailResponse(utils.Fail), nil
|
||||
}
|
||||
return common.OkResponse(nil), nil
|
||||
}
|
||||
@@ -106,33 +106,24 @@ func (l *UserImproveLogic) updatePlatformUser(loginUser utils.UserInfo, bizId in
|
||||
return utils.ErrorParams
|
||||
}
|
||||
|
||||
name := req.Name
|
||||
if name == utils.StringEmpty {
|
||||
name = info.Name
|
||||
}
|
||||
avatar := req.Avatar
|
||||
if avatar == utils.StringEmpty {
|
||||
avatar = info.Avatar
|
||||
}
|
||||
gender := uint32(req.Gender)
|
||||
if gender == utils.NumberZero {
|
||||
gender = info.Gender
|
||||
}
|
||||
birthday := req.Birthday
|
||||
if birthday == utils.StringEmpty {
|
||||
birthday = info.Birthday
|
||||
}
|
||||
|
||||
updateReq := &userpb.InternalUpdateReq{
|
||||
BizId: bizId,
|
||||
SubjectType: "user",
|
||||
Mobile: mobile,
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
Gender: gender,
|
||||
Birthday: birthday,
|
||||
ClientCode: []string{loginUser.ClientCode},
|
||||
}
|
||||
if req.Name != utils.StringEmpty {
|
||||
updateReq.Name = req.Name
|
||||
}
|
||||
if req.Avatar != utils.StringEmpty {
|
||||
updateReq.Avatar = req.Avatar
|
||||
}
|
||||
if req.Gender > utils.NumberZero {
|
||||
updateReq.Gender = uint32(req.Gender)
|
||||
}
|
||||
if req.Birthday != utils.StringEmpty {
|
||||
updateReq.Birthday = req.Birthday
|
||||
}
|
||||
if req.Password != utils.StringEmpty {
|
||||
updateReq.CredentialType = []string{"password"}
|
||||
updateReq.Secret = req.Password
|
||||
|
||||
@@ -37,3 +37,8 @@ func (s *AnalysisDeviceServer) OssCallback(ctx context.Context, in *analysis.Oss
|
||||
l := analysisdevicelogic.NewOssCallbackLogic(ctx, s.svcCtx)
|
||||
return l.OssCallback(in)
|
||||
}
|
||||
|
||||
func (s *AnalysisDeviceServer) PyCallback(ctx context.Context, in *analysis.PyCallbackReq) (*analysis.Response, error) {
|
||||
l := analysisdevicelogic.NewPyCallbackLogic(ctx, s.svcCtx)
|
||||
return l.PyCallback(in)
|
||||
}
|
||||
|
||||
@@ -147,6 +147,17 @@ func (p DeviceDisplayValidator) GetMessage() validate.ValidatorMessages {
|
||||
}
|
||||
}
|
||||
|
||||
type PyCallbackValidator struct {
|
||||
TaskId string `validate:"required,max=64"`
|
||||
}
|
||||
|
||||
func (p PyCallbackValidator) GetMessage() validate.ValidatorMessages {
|
||||
return validate.ValidatorMessages{
|
||||
"TaskId.required": "task_id 必传",
|
||||
"TaskId.max": "task_id 过长",
|
||||
}
|
||||
}
|
||||
|
||||
type StoreInfoValidator struct {
|
||||
Id int64 `validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
@@ -122,7 +122,8 @@ func (l *InternalUpdateLogic) InternalUpdate(in *user.InternalUpdateReq) (*user.
|
||||
if bErr != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "生日格式不正确")
|
||||
}
|
||||
updates["birthday"] = birthday
|
||||
// 用 YYYY-MM-DD 字符串写入,避免 Edit 的 JSON 往返把 CustomTime 编成带 00:00:00 的 datetime
|
||||
updates["birthday"] = birthday.DateString()
|
||||
}
|
||||
|
||||
oldMobile := row.Mobile
|
||||
|
||||
@@ -289,7 +289,7 @@ type InternalUpdateValidator struct {
|
||||
Name string `validate:"omitempty,max=64"`
|
||||
Avatar string `validate:"omitempty,max=255"`
|
||||
Gender uint32 `validate:"omitempty,oneof=0 1 2 3"`
|
||||
Birthday string `validate:"omitempty,max=12"`
|
||||
Birthday string `validate:"omitempty,max=32"`
|
||||
ClientCode []string `validate:"omitempty,dive,required,max=32"`
|
||||
CredentialType []string `validate:"omitempty"`
|
||||
Identifier string `validate:"omitempty,max=128"`
|
||||
|
||||
Reference in New Issue
Block a user