fusenapi/server/websocket/internal/logic/ws_render_image.go
laodaming 6e716e17f9 fix
2023-08-29 15:04:11 +08:00

515 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package logic
//处理websocket云渲染任务数据
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"fusenapi/constants"
"fusenapi/model/gmodel"
"fusenapi/service/repositories"
"fusenapi/utils/curl"
"fusenapi/utils/hash"
"fusenapi/utils/websocket_data"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gorm"
"strconv"
"time"
)
// 云渲染属性
type extendRenderProperty struct {
renderImageTask map[string]*renderTask //需要渲染的图片任务 key是taskId val 是renderId
renderImageTaskCtlChan chan renderImageControlChanItem //渲染任务新增/回调结果移除任务/更新渲染耗时属性的控制通道由于任务map无法读写并发
renderChan chan []byte //渲染消息入口的缓冲队列
}
// 渲染任务新增移除的控制通道的数据
type renderImageControlChanItem struct {
option int // 0删除 1添加 2修改耗时属性
taskId string //map的key(必须传)
renderId string // map的val(增加任务时候传)
renderNotifyImageUrl string //渲染回调数据(删除任务时候传)
taskProperty renderTask //渲染任务的属性
}
// 渲染任务属性
type renderTask struct {
renderId string //渲染id(新增任务传)
combineImage string //刀版图(新增任务传)
combineTakesTime int64 //合刀版图耗时
uploadCombineImageTakesTime int64 //上传刀版图耗时
unityRenderBeginTime int64 //发送给unity时间
unityRenderEndTime int64 //unity回调结果时间
uploadUnityRenderImageTakesTime int64 //上传unity渲染结果图时间
}
// 发送到渲染缓冲队列
func (w *wsConnectItem) sendToRenderChan(data []byte) {
select {
case <-w.closeChan: //已经关闭
return
case w.extendRenderProperty.renderChan <- data: //发入到缓冲队列
return
case <-time.After(time.Second * 3): //三秒没进入缓冲队列就丢弃
return
}
}
// 渲染发送到组装数据组装数据(缓冲队列)
func (w *wsConnectItem) renderImage() {
defer func() {
if err := recover(); err != nil {
logx.Error("func renderImage err:", err)
}
}()
for {
select {
case <-w.closeChan: //已关闭
return
case data := <-w.extendRenderProperty.renderChan:
w.consumeRenderCache(data)
}
}
}
// 消费渲染缓冲数据
func (w *wsConnectItem) consumeRenderCache(data []byte) {
logx.Info("消费渲染数据:", string(data))
var renderImageData websocket_data.RenderImageReqMsg
if err := json.Unmarshal(data, &renderImageData); err != nil {
w.incomeDataFormatErrResponse("invalid format of render data:" + string(data))
logx.Error("invalid format of websocket render image message", err)
return
}
//获取上传最近的logo
userMaterial, err := w.logic.svcCtx.AllModels.FsUserMaterial.FindLatestOne(w.logic.ctx, w.userId, w.guestId)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "failed to get user logo", w.userId, w.guestId, 0, 0, 0, 0)
logx.Error("failed to get user logo")
return
}
//使用默认logo(id=0)
userMaterialDefault, err := w.logic.svcCtx.AllModels.FsUserMaterial.FindOneById(w.logic.ctx, 0)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "default logo is not exists", w.userId, w.guestId, 0, 0, 0, 0)
return
}
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "failed to get default logo", w.userId, w.guestId, 0, 0, 0, 0)
logx.Error("default logo is not exists")
return
}
renderImageData.RenderData.Logo = *userMaterialDefault.ResourceUrl
} else {
renderImageData.RenderData.Logo = *userMaterial.ResourceUrl
}
//用户id赋值
renderImageData.RenderData.UserId = w.userId
renderImageData.RenderData.GuestId = w.guestId
var productSize *gmodel.FsProductSize
//指定尺寸
if renderImageData.RenderData.SizeId > 0 {
productSize, err = w.logic.svcCtx.AllModels.FsProductSize.FindOneByIdProductId(w.logic.ctx, renderImageData.RenderData.SizeId, renderImageData.RenderData.ProductId)
} else { //获取产品第一个尺寸
productSize, err = w.logic.svcCtx.AllModels.FsProductSize.GetProductFirstSize(w.logic.ctx, renderImageData.RenderData.ProductId)
}
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "product first size is not exists", w.userId, w.guestId, 0, 0, renderImageData.RenderData.SizeId, 0)
logx.Error("product size is not found")
return
}
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "failed to get product first size", w.userId, w.guestId, 0, 0, renderImageData.RenderData.SizeId, 0)
logx.Error("failed to get product size:", err)
return
}
//获取模型(只是获取id)
model3dInfo, err := w.logic.svcCtx.AllModels.FsProductModel3d.GetOneBySizeIdTag(w.logic.ctx, productSize.Id, constants.TAG_MODEL, "id")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "product model is not exists", w.userId, w.guestId, 0, 0, productSize.Id, 0)
logx.Error("product model is not found")
return
}
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "failed to get product model", w.userId, w.guestId, 0, 0, productSize.Id, 0)
logx.Error("failed to get product model:", err)
return
}
//获取模板
productTemplate, err := w.logic.svcCtx.AllModels.FsProductTemplateV2.FindFirstOneCloudRenderByProductIdModelIdTemplateTag(w.logic.ctx, renderImageData.RenderData.ProductId, model3dInfo.Id, renderImageData.RenderData.TemplateTag)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "product template is not exists", w.userId, w.guestId, 0, model3dInfo.Id, productSize.Id, 0)
logx.Error("template info is not found")
return
}
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "failed to get product template", w.userId, w.guestId, 0, model3dInfo.Id, productSize.Id, 0)
logx.Error("failed to get template info:", err)
return
}
//获取渲染设置信息
element, err := w.logic.svcCtx.AllModels.FsProductTemplateElement.FindOneByModelId(w.logic.ctx, *productTemplate.ElementModelId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "render element is not exists", w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
logx.Error("element info is not found,element_model_id = ", 0)
return
}
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "failed to get render element", w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
logx.Error("failed to get element ,", err)
return
}
//获取刀版图
combineReq := repositories.LogoCombineReq{
UserId: renderImageData.RenderData.UserId,
GuestId: renderImageData.RenderData.GuestId,
TemplateId: productTemplate.Id,
TemplateTag: renderImageData.RenderData.TemplateTag,
Website: renderImageData.RenderData.Website,
Slogan: renderImageData.RenderData.Slogan,
Address: renderImageData.RenderData.Address,
Phone: renderImageData.RenderData.Phone,
}
res, err := w.logic.svcCtx.Repositories.ImageHandle.LogoCombine(w.logic.ctx, &combineReq)
if err != nil {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "failed to combine image:"+err.Error(), w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
logx.Error("合成刀版图失败,合成请求数据:", combineReq, "错误信息:", err)
return
}
combineImage := "" //刀版图
if res != nil && res.ResourceUrl != nil {
combineImage = *res.ResourceUrl
} else {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, "", "combine image is empty", w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
logx.Error("合成刀版图失败,合成的刀版图是空指针:", err)
return
}
logx.Info("合成刀版图成功,合成刀版图数据:", combineReq, ",logo图片:", renderImageData.RenderData.Logo, " 刀版图:", *res.ResourceUrl)
//如果是传入了指定尺寸,则直接返回刀版图
if renderImageData.RenderData.SizeId > 0 {
w.sendToOutChan(w.respondDataFormat(constants.WEBSOCKET_COMBINE_IMAGE, websocket_data.CombineImageRspMsg{
RenderId: renderImageData.RenderId,
CombineImage: combineImage,
}))
return
}
//获取唯一id
taskId := w.genRenderTaskId(renderImageData, model3dInfo, productTemplate, element)
//查询有没有缓存的资源,有就返回######################
resource, err := w.logic.svcCtx.AllModels.FsResource.FindOneById(w.logic.ctx, taskId)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
w.renderErrResponse(renderImageData.RenderId, renderImageData.RenderData.TemplateTag, taskId, "failed to get render cache", w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
logx.Error("failed to find render resource:", err)
return
}
} else {
//返回给客户端
b := w.respondDataFormat(constants.WEBSOCKET_RENDER_IMAGE, websocket_data.RenderImageRspMsg{
RenderId: renderImageData.RenderId,
Image: *resource.ResourceUrl,
CombineImage: combineImage,
RenderProcessTime: websocket_data.RenderProcessTime{
CombineTakesTime: "cache",
UnityRenderTakesTime: "cache",
UploadCombineImageTakesTime: "cache",
UploadUnityRenderImageTakesTime: "cache",
},
})
//发送数据到out chan
w.sendToOutChan(b)
return
}
//###########################################
//把需要渲染的图片任务加进去
w.createRenderTask(renderImageControlChanItem{
option: 1, //0删除 1添加 2修改耗时属性
taskId: taskId,
renderId: renderImageData.RenderId,
})
//记录刀版图合成消耗时间跟上传刀版图时间以及刀版图
w.modifyRenderTaskProperty(renderImageControlChanItem{
option: 2,
taskId: taskId,
taskProperty: renderTask{
combineTakesTime: res.DiffTimeLogoCombine,
uploadCombineImageTakesTime: res.DiffTimeUploadFile,
combineImage: combineImage,
},
})
//组装数据
if err = w.assembleRenderData(taskId, combineImage, renderImageData, productTemplate, model3dInfo, element, productSize); err != nil {
logx.Error("组装数据失败:", err)
return
}
}
// 组装渲染任务id
func (w *wsConnectItem) genRenderTaskId(renderImageData websocket_data.RenderImageReqMsg, model3dInfo *gmodel.FsProductModel3d, productTemplate *gmodel.FsProductTemplateV2, element *gmodel.FsProductTemplateElement) string {
//生成任务id(需要把user_id,guest_id设为0)
incomeHashParam := renderImageData.RenderData
incomeHashParam.UserId = 0 //设为0(渲染跟用户id无关)
incomeHashParam.GuestId = 0 //设为0(渲染跟用户id无关)
incomeHashBytes, _ := json.Marshal(incomeHashParam)
modelHashStr := ""
templateHashStr := ""
if model3dInfo.ModelInfo != nil {
modelHashStr = *model3dInfo.ModelInfo
}
if productTemplate.TemplateInfo != nil {
templateHashStr = *productTemplate.TemplateInfo
}
elementHashBytes, _ := json.Marshal(element)
hashMap := map[string]interface{}{
"income_param": incomeHashBytes,
"model_info": modelHashStr,
"template_info": templateHashStr,
"material_image": *productTemplate.MaterialImg,
"render_element": elementHashBytes,
}
return hash.JsonHashKey(hashMap)
}
// 组装数据发送给unity
func (w *wsConnectItem) assembleRenderData(taskId string, combineImage string, info websocket_data.RenderImageReqMsg, productTemplate *gmodel.FsProductTemplateV2, model3dInfo *gmodel.FsProductModel3d, element *gmodel.FsProductTemplateElement, productSize *gmodel.FsProductSize) (err error) {
//组装数据
refletion := -1
if element.Refletion != nil && *element.Refletion != "" {
refletion, err = strconv.Atoi(*element.Refletion)
if err != nil {
logx.Error("err refletion:set default -1")
w.renderErrResponse(info.RenderId, info.RenderData.TemplateTag, taskId, "parse element.Refletion from string to number err", w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
return err
}
}
//组装data数据
var mode map[string]interface{}
if element.Mode != nil && *element.Mode != "" {
if err = json.Unmarshal([]byte(*element.Mode), &mode); err != nil {
logx.Error("faile to parse element mode json:", err)
w.renderErrResponse(info.RenderId, info.RenderData.TemplateTag, taskId, "parse element.Mode err", w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
return err
}
}
tempData := make([]map[string]interface{}, 0, 3)
if element.Base != nil && *element.Base != "" {
tempData = append(tempData, map[string]interface{}{
"name": "model",
"data": "0," + combineImage + "," + *element.Base,
"type": "other",
"layer": "0",
"is_update": 1,
"mode": mode["model"],
})
}
if element.Shadow != nil && *element.Shadow != "" {
tempData = append(tempData, map[string]interface{}{
"name": "shadow",
"data": *element.Shadow,
"type": "other",
"layer": "0",
"is_update": 0,
"mode": mode["shadow"],
})
}
if element.ModelP != nil && *element.ModelP != "" {
tempData = append(tempData, map[string]interface{}{
"name": "model_P",
"data": "0," + *element.ModelP,
"type": "other",
"layer": "0",
"is_update": 0,
"mode": mode["model_P"],
})
}
result := []interface{}{
map[string]interface{}{
"light": *element.Light,
"refletion": refletion,
"scale": *element.Scale,
"sku_id": info.RenderData.ProductId,
"tid": *element.Title,
"rotation": *element.Rotation,
"filePath": "", //todo 文件路径,针对千人千面
"data": tempData,
},
}
sendData := map[string]interface{}{
"id": taskId,
"order_id": 0,
"user_id": info.RenderData.UserId,
"guest_id": info.RenderData.GuestId,
"sku_ids": []int64{info.RenderData.ProductId},
"tids": []string{*element.Title},
"data": result,
"is_thousand_face": 0,
"folder": "", //todo 千人千面需要使用
}
//请求unity接口
url := w.logic.svcCtx.Config.Unity.Host + "/api/render/queue/push"
header := make(map[string]string)
header["content-type"] = "application/json"
postData := map[string]interface{}{
"group": "unity3d",
"source": "home page",
"priority": 1,
"create_at": time.Now().UTC(),
"render_data": sendData,
}
postDataBytes, _ := json.Marshal(postData)
unityRenderBeginTime := time.Now().UTC().UnixMilli()
_, err = curl.ApiCall(url, "POST", header, bytes.NewReader(postDataBytes), time.Second*10)
if err != nil {
w.renderErrResponse(info.RenderId, info.RenderData.TemplateTag, taskId, "request unity api err", w.userId, w.guestId, productTemplate.Id, model3dInfo.Id, productSize.Id, *productTemplate.ElementModelId)
logx.Error("failed to send data to unity")
return err
}
//记录发送到unity时间
w.modifyRenderTaskProperty(renderImageControlChanItem{
option: 2,
taskId: taskId,
taskProperty: renderTask{
unityRenderBeginTime: unityRenderBeginTime,
},
})
logx.Info("发送到unity成功,刀版图:", combineImage, " 请求unity的数据:", string(postDataBytes))
return nil
}
// 增加渲染任务
func (w *wsConnectItem) createRenderTask(data renderImageControlChanItem) {
//强制设为增加
data.option = 1
select {
case <-w.closeChan: //关闭
return
case w.extendRenderProperty.renderImageTaskCtlChan <- data:
return
case <-time.After(time.Second * 3):
return
}
}
// 渲染回调处理并删除渲染任务
func (w *wsConnectItem) deleteRenderTask(data renderImageControlChanItem) {
//强制设为删除
data.option = 0
select {
case <-w.closeChan: //关闭
return
case w.extendRenderProperty.renderImageTaskCtlChan <- data:
return
case <-time.After(time.Second * 3):
return
}
}
// 修改任务属性(只有耗时属性可以更新)
func (w *wsConnectItem) modifyRenderTaskProperty(data renderImageControlChanItem) {
if data.taskId == "" {
logx.Error("设置耗时属性需要的task_id不能为空")
return
}
//强制设为修改耗时属性
data.option = 2
select {
case <-w.closeChan: //关闭
return
case w.extendRenderProperty.renderImageTaskCtlChan <- data:
return
case <-time.After(time.Second * 3):
return
}
}
// 处理渲染任务的增加/删除/修改耗时属性任务map不能读写并发所以放在chan里面串行执行
func (w *wsConnectItem) operationRenderTask() {
defer func() {
if err := recover(); err != nil {
logx.Error("operation render task panic:", err)
}
}()
for {
select {
case <-w.closeChan:
return
case data := <-w.extendRenderProperty.renderImageTaskCtlChan:
switch data.option {
case 0: //渲染结果回调,删除任务
//存在任务,则发送渲染结果给前端
if taskData, ok := w.extendRenderProperty.renderImageTask[data.taskId]; ok {
CombineTakesTime := "0ms"
UnityRenderTakesTime := "0ms"
uploadCombineImageTakesTime := "0ms"
uploadUnityRenderImageTakesTime := "0ms"
//合图时间
if taskData.combineTakesTime > 0 {
CombineTakesTime = fmt.Sprintf("%dms", taskData.combineTakesTime)
}
//上传刀版图时间
if taskData.uploadCombineImageTakesTime > 0 {
uploadCombineImageTakesTime = fmt.Sprintf("%dms", taskData.uploadCombineImageTakesTime)
}
//unity渲染时间
if taskData.unityRenderBeginTime > 0 && taskData.unityRenderEndTime > 0 {
UnityRenderTakesTime = fmt.Sprintf("%dms", taskData.unityRenderEndTime-taskData.unityRenderBeginTime)
}
//上传unity渲染图耗时
if taskData.uploadUnityRenderImageTakesTime > 0 {
uploadUnityRenderImageTakesTime = fmt.Sprintf("%dms", taskData.uploadUnityRenderImageTakesTime)
}
//发送到出口
w.sendToOutChan(w.respondDataFormat(constants.WEBSOCKET_RENDER_IMAGE, websocket_data.RenderImageRspMsg{
RenderId: taskData.renderId,
Image: data.renderNotifyImageUrl,
CombineImage: taskData.combineImage,
RenderProcessTime: websocket_data.RenderProcessTime{
CombineTakesTime: CombineTakesTime,
UnityRenderTakesTime: UnityRenderTakesTime,
UploadCombineImageTakesTime: uploadCombineImageTakesTime,
UploadUnityRenderImageTakesTime: uploadUnityRenderImageTakesTime,
},
}))
}
//删除任务
delete(w.extendRenderProperty.renderImageTask, data.taskId)
case 1: //新增任务
w.extendRenderProperty.renderImageTask[data.taskId] = &renderTask{
renderId: data.renderId,
}
case 2: //修改任务属性
if taskData, ok := w.extendRenderProperty.renderImageTask[data.taskId]; ok {
//合图耗时
if data.taskProperty.combineTakesTime != 0 {
taskData.combineTakesTime = data.taskProperty.combineTakesTime
}
//上传合图耗时
if data.taskProperty.uploadCombineImageTakesTime != 0 {
taskData.uploadCombineImageTakesTime = data.taskProperty.uploadCombineImageTakesTime
}
//上传渲染结果图耗时
if data.taskProperty.uploadUnityRenderImageTakesTime != 0 {
taskData.uploadUnityRenderImageTakesTime = data.taskProperty.uploadUnityRenderImageTakesTime
}
//发送unity时间
if data.taskProperty.unityRenderBeginTime != 0 {
taskData.unityRenderBeginTime = data.taskProperty.unityRenderBeginTime
}
//收到unity返回的时间
if data.taskProperty.unityRenderEndTime != 0 {
taskData.unityRenderEndTime = data.taskProperty.unityRenderEndTime
}
if data.taskProperty.combineImage != "" {
taskData.combineImage = data.taskProperty.combineImage
}
}
}
}
}
}