Ver código fonte

Merge branch 'cutomer_template_1215' into smart-shooter-clothing

rambo 5 horas atrás
pai
commit
148ca2905b
37 arquivos alterados com 2417 adições e 358 exclusões
  1. 338 0
      generateServer.API.md
  2. 454 86
      python/api.py
  3. 18 0
      python/canvas_json.json
  4. 4 1
      python/config.ini
  5. 1 0
      python/custom_plugins/plugins_mode/detail_generate_base.py
  6. 33 6
      python/databases.py
  7. 18 16
      python/detail_template_test_xinnuo.json
  8. 35 0
      python/docs/socket命令.md
  9. 97 49
      python/mcu/DeviceControl.py
  10. 1 1
      python/mcu/Mcu.py
  11. 84 8
      python/mcu/ProgramItem.py
  12. 10 0
      python/mcu/RemoteControlV2.py
  13. 2 2
      python/mcu/SerialIns.py
  14. 1 9
      python/mcu/capture/smart_shooter_class.py
  15. 105 0
      python/mcu_test.py
  16. 1 0
      python/middleware.py
  17. 8 6
      python/model/photo_record.py
  18. 28 2
      python/models.py
  19. 30 6
      python/service/auto_deal_pics/base_deal.py
  20. 32 5
      python/service/base_deal.py
  21. 391 0
      python/service/customer_template_service.py
  22. 11 11
      python/service/data.py
  23. 1 1
      python/service/deal_one_image.py
  24. 10 11
      python/service/generate_goods_no_detail_pic/data.py
  25. 6 8
      python/service/generate_main_image/grenerate_main_image_test.py
  26. 171 53
      python/service/grenerate_main_image_test.py
  27. 173 1
      python/service/image_deal_base_func.py
  28. 36 2
      python/service/match_and_cutout_mode_control/base_deal_image_v2.py
  29. 7 1
      python/service/multi_threaded_image_saving.py
  30. 14 8
      python/service/online_request/module_online_data.py
  31. 41 1
      python/service/remove_bg_ali.py
  32. 65 12
      python/service/remove_bg_pixian.py
  33. 49 34
      python/service/run_main.py
  34. 76 3
      python/settings.py
  35. 38 8
      python/sockets/message_handler.py
  36. 2 1
      python/sockets/socket_server.py
  37. 26 6
      python/temp.py

+ 338 - 0
generateServer.API.md

@@ -0,0 +1,338 @@
+# 图片生成接口文档
+
+## 接口概述
+
+轻量级 HTTP 图片生成服务,用于根据画布配置和商品数据生成营销图片。该接口返回 base64 格式的图片数据,不做文件落地,便于外部(如 Python)直接调用获取图片切片。
+
+**运行环境**: 仅在 Electron 渲染进程中可用(需要 Node.js 能力)
+
+---
+
+## 接口信息
+
+### 基本信息
+
+- **接口地址**: `http://localhost:3001/generate`
+- **请求方法**: `POST`
+- **Content-Type**: `application/json`
+- **请求体大小限制**: 最大 50MB
+
+### 接口路径
+
+```
+POST /generate
+```
+
+---
+
+## 请求参数
+
+### 请求体结构
+
+```json
+{
+  "canvasList": [],  // 画布配置数组(必填)
+  "goodsList": []    // 商品数据数组(必填)
+}
+```
+
+### 参数说明
+
+#### 1. canvasList(画布配置数组)
+
+画布配置数组,每个元素代表一个画布模板。
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| index | number | 是 | 画布索引,从 0 开始 |
+| canvas_json | string | 是 | Fabric.js 画布配置的 JSON 字符串,或特殊值 `"scene"`(场景图) |
+| width | number | 是 | 画布宽度(像素) |
+| height | number | 是 | 画布高度(像素) |
+| bg_color | string | 是 | 背景色,如 `"#fff"` |
+| name | string | 否 | 画布名称 |
+| preview | string | 否 | 预览图 URL |
+| tpl_url | string | 否 | 模板 URL |
+| image_path | string | 否 | 图片路径 |
+| canvas_type | string | 否 | 画布类型:`"normal"`(普通)、`"model"`(模特图)、`"scene"`(场景图) |
+| multi_goods_mode | string | 否 | 多商品模式 |
+| max_goods_count | number \| null | 否 | 最大商品数量 |
+
+**canvas_json 说明**:
+- 普通画布:Fabric.js 格式的 JSON 字符串,包含 `version`、`objects`、`background` 等字段
+- 场景图:直接传入字符串 `"scene"`
+
+#### 2. goodsList(商品数据数组)
+
+商品数据数组,每个元素是一个对象,键为款号(styleKey),值为商品信息。
+
+**数据结构**:
+
+```json
+{
+  "款号(styleKey)": {
+    "款号": "string",           // 款号,如 "E305-01003"
+    "设计理念": "string",        // 设计理念文案(可选,款级)
+    "货号资料": [                // 货号数组
+      {
+        "货号": "string",        // 货号,如 "A596351"
+        "颜色": "string",        // 颜色,如 "黑色"
+        "设计理念": "string",    // 设计理念文案(可选,货号级,优先级高于款级)
+        "pics": {                // 图片路径对象
+          "视角名称": "string"   // 键为视角名称(如"俯视"、"侧视"、"后跟"等),值为图片路径
+        }
+      }
+    ]
+  }
+}
+```
+
+**pics 对象说明**:
+- 键:视角名称,如 `"俯视"`、`"侧视"`、`"后跟"`、`"鞋底"`、`"内里"` 等
+- 值:图片文件路径(支持本地路径,如 `"C:\\Users\\...\\image.png"`)
+
+---
+
+## 响应格式
+
+### 成功响应
+
+**HTTP 状态码**: `200`
+
+**响应体**:
+
+```json
+{
+  "code": 0,
+  "images": [
+    {
+      "canvasIndex": 0,                    // 画布索引
+      "dataUrl": "data:image/png;base64,..."  // base64 格式的图片数据
+    }
+  ],
+  "plans": []  // 渲染计划数组(内部使用)
+}
+```
+
+**响应字段说明**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| code | number | 响应码,0 表示成功 |
+| images | array | 生成的图片数组 |
+| images[].canvasIndex | number | 对应的画布索引 |
+| images[].dataUrl | string | base64 格式的图片数据,可直接用于 `<img src="...">` |
+| plans | array | 渲染计划数组(内部数据结构) |
+
+### 失败响应
+
+**HTTP 状态码**: `500`
+
+**响应体**:
+
+```json
+{
+  "code": 1,
+  "msg": "error message"
+}
+```
+
+**响应字段说明**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| code | number | 响应码,1 表示失败 |
+| msg | string | 错误信息 |
+
+### 其他错误响应
+
+| HTTP 状态码 | 说明 | 响应体 |
+|------------|------|--------|
+| 404 | 路径或方法错误 | `"Not Found"` |
+| 413 | 请求体过大(>50MB) | `"Payload Too Large"` |
+
+---
+
+## 调用示例
+
+### JavaScript/TypeScript 示例
+
+```javascript
+const response = await fetch('http://localhost:3001/generate', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json'
+  },
+  body: JSON.stringify({
+    canvasList: [
+      {
+        index: 0,
+        canvas_json: '{"version":"5.2.1","objects":[{"type":"image","src":"...","left":0,"top":0}],"background":"#fff"}',
+        width: 395,
+        height: 618,
+        bg_color: "#fff",
+        name: "画布_1",
+        canvas_type: "normal"
+      }
+    ],
+    goodsList: [
+      {
+        "AC5120913": {
+          "款号": "E305-01003",
+          "设计理念": "平衡舒适性、美观性、功能性和工艺品质",
+          "货号资料": [
+            {
+              "货号": "A596351",
+              "颜色": "黑色",
+              "设计理念": "满足现代消费者在不同场景下的需求",
+              "pics": {
+                "俯视": "C:\\Users\\Administrator\\Desktop\\img\\A596351\\1.png",
+                "侧视": "C:\\Users\\Administrator\\Desktop\\img\\A596351\\2.png",
+                "后跟": "C:\\Users\\Administrator\\Desktop\\img\\A596351\\3.png",
+                "鞋底": "C:\\Users\\Administrator\\Desktop\\img\\A596351\\4.png",
+                "内里": "C:\\Users\\Administrator\\Desktop\\img\\A596351\\5.png"
+              }
+            }
+          ]
+        }
+      }
+    ]
+  })
+})
+
+const result = await response.json()
+if (result.code === 0) {
+  console.log('生成成功,共', result.images.length, '张图片')
+  result.images.forEach(img => {
+    console.log(`画布 ${img.canvasIndex} 的图片:`, img.dataUrl.substring(0, 50) + '...')
+  })
+} else {
+  console.error('生成失败:', result.msg)
+}
+```
+
+### Python 示例
+
+```python
+import requests
+import json
+
+url = "http://localhost:3001/generate"
+
+payload = {
+    "canvasList": [
+        {
+            "index": 0,
+            "canvas_json": '{"version":"5.2.1","objects":[],"background":"#fff"}',
+            "width": 395,
+            "height": 618,
+            "bg_color": "#fff",
+            "name": "画布_1",
+            "canvas_type": "normal"
+        }
+    ],
+    "goodsList": [
+        {
+            "AC5120913": {
+                "款号": "E305-01003",
+                "货号资料": [
+                    {
+                        "货号": "A596351",
+                        "颜色": "黑色",
+                        "pics": {
+                            "俯视": "C:\\Users\\Administrator\\Desktop\\img\\A596351\\1.png",
+                            "侧视": "C:\\Users\\Administrator\\Desktop\\img\\A596351\\2.png"
+                        }
+                    }
+                ]
+            }
+        }
+    ]
+}
+
+response = requests.post(url, json=payload)
+
+if response.status_code == 200:
+    result = response.json()
+    if result.get("code") == 0:
+        print(f"生成成功,共 {len(result['images'])} 张图片")
+        for img in result["images"]:
+            print(f"画布 {img['canvasIndex']} 的图片已生成")
+            # 可以保存 base64 图片
+            # data_url = img["dataUrl"]
+            # base64_data = data_url.split(",")[1]
+            # with open(f"output_{img['canvasIndex']}.png", "wb") as f:
+            #     f.write(base64.b64decode(base64_data))
+    else:
+        print(f"生成失败: {result.get('msg')}")
+else:
+    print(f"请求失败,状态码: {response.status_code}")
+```
+
+### cURL 示例
+
+```bash
+curl -X POST http://localhost:3001/generate \
+  -H "Content-Type: application/json" \
+  -d '{
+    "canvasList": [
+      {
+        "index": 0,
+        "canvas_json": "{\"version\":\"5.2.1\",\"objects\":[],\"background\":\"#fff\"}",
+        "width": 395,
+        "height": 618,
+        "bg_color": "#fff",
+        "name": "画布_1",
+        "canvas_type": "normal"
+      }
+    ],
+    "goodsList": [
+      {
+        "AC5120913": {
+          "款号": "E305-01003",
+          "货号资料": [
+            {
+              "货号": "A596351",
+              "颜色": "黑色",
+              "pics": {
+                "俯视": "C:\\\\Users\\\\Administrator\\\\Desktop\\\\img\\\\A596351\\\\1.png",
+                "侧视": "C:\\\\Users\\\\Administrator\\\\Desktop\\\\img\\\\A596351\\\\2.png"
+              }
+            }
+          ]
+        }
+      }
+    ]
+  }'
+```
+
+---
+
+## 注意事项
+
+1. **运行环境限制**: 该接口仅在 Electron 渲染进程中可用,需要 Node.js 集成能力。在普通浏览器环境中无法使用。
+
+2. **端口占用**: 接口默认监听 `3001` 端口,确保该端口未被占用。
+
+3. **请求体大小**: 请求体大小限制为 50MB,超过此限制将返回 `413` 错误。
+
+4. **图片路径**: 
+   - 支持本地文件路径(Windows 路径格式:`C:\\Users\\...`)
+   - 确保图片文件存在且可访问
+
+5. **canvas_json 格式**: 
+   - 普通画布必须是有效的 Fabric.js JSON 字符串
+   - 场景图使用特殊值 `"scene"`
+
+6. **错误处理**: 建议在调用时捕获异常,并根据 `code` 字段判断请求是否成功。
+
+7. **base64 图片使用**: 
+   - 返回的 `dataUrl` 可直接用于 HTML `<img>` 标签的 `src` 属性
+   - 如需保存为文件,需要提取 base64 数据部分(去掉 `data:image/png;base64,` 前缀)
+
+---
+
+## 版本信息
+
+- **接口版本**: v1.0
+- **最后更新**: 2024-12-09
+

+ 454 - 86
python/api.py

@@ -1,4 +1,4 @@
-from re import search
+from re import search,match
 from natsort.natsort import order_by_index
 from sqlalchemy import func
 from models import *
@@ -35,6 +35,8 @@ import functools
 import traceback,stat
 import concurrent.futures
 from sockets.message_handler import handlerFolderDelete
+from service.remove_bg_ali import RemoveBgALi
+import uuid as mine_uuid
 def log_exception_with_context(context_message=""):
     """装饰器:为函数添加异常日志上下文"""
 
@@ -142,27 +144,28 @@ async def forwardRequest(request: HlmForwardRequest):
 
 def __createExcelGoodsArray(excel_path):
     '''创建通过excel形式得货号组数据'''
-    excel_df = pd.read_excel(excel_path, sheet_name=0, header=0)
-    if "文件夹名称" not in excel_df.columns:
-        raise UnicornException("缺失 [文件夹名称] 列")
-    if "商品货号" not in excel_df.columns:
-        raise UnicornException("缺失 [商品货号] 列")
-    if "款号" not in excel_df.columns:
-        raise UnicornException("缺失 [款号] 列")
-    goods_art_dirs = excel_df.groupby(excel_df["款号"])
-    # 抠图时用到的货号列表,与生成详情图有所区别
-    goods_art_no_arrays = []
-    # # 详情图生成需要对同款商品进行分组,保证详情图可以生成多个色
-    goods_art_no_group_arrays = []
-    for _, goods_row in excel_df.iterrows():
-        goods_art_no = str(goods_row["商品货号"])
-        goods_art_no_arrays.append(goods_art_no)
-        goods_no = str(goods_row["款号"])
-        a001_df = goods_art_dirs.get_group(goods_no)
-        goods_art_groups = a001_df["商品货号"].tolist()
-        if goods_art_groups in goods_art_no_group_arrays:
-            continue
-        goods_art_no_group_arrays.append(goods_art_groups)
+    try:
+        excel_df = pd.read_excel(excel_path, sheet_name=0, header=0)
+        if "商品货号" not in excel_df.columns:
+            raise UnicornException("缺失 [商品货号] 列")
+        if "款号" not in excel_df.columns:
+            raise UnicornException("缺失 [款号] 列")
+        goods_art_dirs = excel_df.groupby(excel_df["款号"])
+        # 抠图时用到的货号列表,与生成详情图有所区别
+        goods_art_no_arrays = []
+        # # 详情图生成需要对同款商品进行分组,保证详情图可以生成多个色
+        goods_art_no_group_arrays = []
+        for _, goods_row in excel_df.iterrows():
+            goods_art_no = str(goods_row["商品货号"])
+            goods_art_no_arrays.append(goods_art_no)
+            goods_no = str(goods_row["款号"])
+            a001_df = goods_art_dirs.get_group(goods_no)
+            goods_art_groups = a001_df["商品货号"].tolist()
+            if goods_art_groups in goods_art_no_group_arrays:
+                continue
+            goods_art_no_group_arrays.append(goods_art_groups)
+    except Exception as e:
+        raise UnicornException("Excel文件解析失败,请检查是否缺少列")
     return goods_art_no_arrays,excel_df,goods_art_no_group_arrays
 
 
@@ -247,7 +250,6 @@ async def process_handle_detail(request: Request, params: HandlerDetail):
         # 初始化基础变量
         handler_result = []
         handler_result_folder = ""
-        
         # 处理参数
         obj, token, uuid = None, "Bearer " + params.token, params.uuid
         aigc_clazz = AIGCDataRequest(token)
@@ -284,14 +286,12 @@ async def process_handle_detail(request: Request, params: HandlerDetail):
         handler_result_folder, handler_result = await _process_cutout(
             run_main, config_data, goods_art_no_arrays, move_folder_array
         )
-            
         # 处理场景图和模特图
-        return_data_check_before_detail = run_main.check_before_detail(config_data)
+        return_data_check_before_detail = run_main.check_before_detail(config_data,is_detail)
         
         # 检查处理结果
         success_handler = return_data_check_before_detail.get("data", {}).get("config_data", {}).get("success_handler", [])
         failed_items = [item for item in success_handler if item.get('success') == False]
-        
         if failed_items:
             await sendAsyncMessage(
                 msg="处理结束",
@@ -334,7 +334,8 @@ async def process_handle_detail(request: Request, params: HandlerDetail):
         if is_detail == 1:
             handler_result_folder, handler_result= await _process_detail_pages(
                 run_main, return_data_check_before_detail, onlineData, 
-                online_stores, goods_art_no_arrays, handler_result_folder
+                online_stores, goods_art_no_arrays, handler_result_folder,
+                params
             )
             # 如果需要上传到第三方平台
             if online_stores:
@@ -351,7 +352,7 @@ async def process_handle_detail(request: Request, params: HandlerDetail):
         else:
             await sendAsyncMessage(
                 msg="处理结束",
-                data={"output_folder": f"{handler_result_folder}/{current_day}", "list": handler_result},
+                data={"output_folder": f"{handler_result_folder}", "list": handler_result},
                 status="处理结束",
                 msg_type="detail_result_progress",
             )
@@ -382,7 +383,7 @@ async def _process_non_excel_mode(params, goods_art_no_arrays):
             
         session = SqlQuery()
         pr = CRUD(PhotoRecord)
-        images = pr.read_all(session, conditions={"goods_art_no": goods_art_no})
+        images = pr.read_all(session, conditions={"goods_art_no": goods_art_no,"delete_time": None})
         session.close()
         if not images:
             raise UnicornException(
@@ -408,20 +409,19 @@ async def _process_excel_mode(goods_art_no_arrays,excel_df):
     move_folder_array = check_move_goods_art_no_folder("output", goods_art_no_arrays, limit_path)
     session = SqlQuery()
     for index, row in excel_df.iterrows():
-            goods_art_no_image_dir = str(row["文件夹名称"])
             goods_art_no = str(row["商品货号"])
             print("货号数据", goods_art_no)
             if not goods_art_no:
                 raise UnicornException("货号不能为空")
             pr = CRUD(PhotoRecord)
-            images = pr.read_all(session, conditions={"goods_art_no": goods_art_no})
+            images = pr.read_all(session, conditions={"goods_art_no": goods_art_no,"delete_time": None})
             if not images:
                 raise UnicornException(
                     f"商品货号【{goods_art_no}】未查询到拍摄记录,请检查表格中的货号数据列"
                 )
             # 货号目录不存在再去进行移动和创建操作
             if move_folder_array.get(goods_art_no) is None:
-                await _process_image_copy_and_move(goods_art_no_image_dir, images,True)
+                await _process_image_copy_and_move(goods_art_no, images,True)
     session.close()
     return move_folder_array
 
@@ -457,9 +457,9 @@ async def _build_config_data(params, goods_art_no_arrays):
     """构建配置数据"""
     temp_class = {}
     temp_name_list = []
-    
+    # 模板类型;0系统模板;1自定义模板
     for tempItem in params.temp_list:
-        temp_class[tempItem.template_id] = tempItem.template_local_classes
+        temp_class[tempItem.template_id] = {"class_path":tempItem.template_local_classes,"template_type":tempItem.template_type if tempItem.template_type else 0}
         temp_name_list.append(tempItem.template_id)
         
     cutOutMode = (
@@ -473,7 +473,7 @@ async def _build_config_data(params, goods_art_no_arrays):
     config_data = {
         "image_dir": limit_path,
         "image_order": (
-            "俯视,侧视,后跟,鞋底,内里,组合,组合2,组合3,组合4,组合5"
+            "俯视,侧视,后跟,鞋底,内里,组合,组合2,组合3,组合4,组合5,组合6,组合7,组合8,组合9,组合10,组合11,组合12,组合13,组合14,组合15,组合16,组合17,组合18,组合19,组合20,组合21,组合22,组合23,组合24,组合25,组合26"
             if not params.template_image_order
             else params.template_image_order
         ),
@@ -499,18 +499,22 @@ async def _build_config_data(params, goods_art_no_arrays):
         "target_error_folder": f"{limit_path}/软件-生成详情错误",
         "success_handler": [],
     }
-    
-    # 动态导入类
+    temp_class_dict = {}
     try:
-        temp_class_dict = {}
-        for key, class_path in config_data["temp_class"].items():
-            module_path, class_name = class_path.rsplit(".", 1)
-            module = importlib.import_module(module_path)
-            cls = getattr(module, class_name)
-            temp_class_dict[key] = cls
+        print("configdata 模板信息",config_data["temp_class"]) 
+        for key, val in config_data["temp_class"].items():
+            class_path = val.get("class_path")
+            template_type = val.get("template_type",0)
+            if template_type == 0:
+                # 如果是系统模板,才进行动态类导入操作
+                module_path, class_name = class_path.rsplit(".", 1)
+                module = importlib.import_module(module_path)
+                cls = getattr(module, class_name)
+                temp_class_dict[key] = {"cls":cls,"template_type":template_type}
+            else:
+                temp_class_dict[key] = {"cls":class_path,"template_type":template_type}
     except:
-      raise UnicornException("详情页模板不存在或未下载完成,请重启软件后重试")
-        
+        raise UnicornException("详情页模板不存在或未下载完成,请重启软件后重试")
     config_data["temp_class"] = temp_class_dict
     return config_data
 
@@ -518,10 +522,24 @@ async def _process_cutout(run_main, config_data, goods_art_no_arrays, move_folde
     """处理抠图"""
     handler_result = []
     handler_result_folder = ""
-    
+    have_handler_keys = move_folder_array.keys()
+    if len(have_handler_keys) >0 :
+        progress = {
+                    "status":"正在处理",
+                    "current":len(have_handler_keys),
+                    "total":len(goods_art_no_arrays),
+                    "error":0,
+                    "goods_art_no":None
+                    }
+        await sendAsyncMessage(
+            msg="正在处理",
+            data=None,
+            status="正在处理",
+            msg_type="segment_progress",
+            progress=progress
+        )
     return_data = run_main.check_before_cutout(config_data)
     cutout_res = run_main.check_for_cutout_image_first_call_back(return_data)
-    
     if cutout_res:
         # sys_path = format(os.getcwd()).replace("\\", "/")
         handler_result_folder = f"{config_data['image_dir']}"
@@ -533,7 +551,7 @@ async def _process_cutout(run_main, config_data, goods_art_no_arrays, move_folde
                 "info": "处理成功",
             })
             
-    if len(move_folder_array.keys()) == len(goods_art_no_arrays):
+    if len(have_handler_keys) == len(goods_art_no_arrays) or (len(have_handler_keys) == 0  and cutout_res):
         handler_result_folder = handler_result_folder.replace("\\", "/")
         success_items = [item for item in handler_result if item.get('success') == True]
         cutout_folder = handler_result_folder+"/"+success_items[0].get("goods_art_no")+"/800x800" if len(success_items) > 0 else ""
@@ -551,7 +569,6 @@ async def _process_cutout(run_main, config_data, goods_art_no_arrays, move_folde
             msg_type="segment_progress",
             progress=progress
         )
-        
     return handler_result_folder, handler_result
 
 async def _process_scene_images(aigc_clazz, run_main, return_data_check_before_detail, product_scene_prompt):
@@ -719,7 +736,7 @@ async def _process_model_images(aigc_clazz, run_main, return_data_check_before_d
         msg_type="upper_footer_progress",
         progress=upper_footer_progress
     )
-    
+    print("上脚图=====>>>>",goods_dict,return_data_check_before_detail)
     for goods_art_no_info in goods_dict.keys():
         goods_art_dict_info = goods_dict.get(goods_art_no_info,None)
         new_goods_dict.setdefault(goods_art_no_info,goods_art_dict_info)
@@ -783,6 +800,7 @@ async def _process_model_images(aigc_clazz, run_main, return_data_check_before_d
                 )
                 
             except (concurrent.futures.TimeoutError, Exception) as e:
+                print("模特图处理异常信息",e)
                 os.remove(save_image_path)
                 # upper_footer_finish_progress-=1
                 upper_footer_error_progress += 1
@@ -826,15 +844,15 @@ async def _process_model_images(aigc_clazz, run_main, return_data_check_before_d
     return return_data_check_before_detail
 
 async def _process_detail_pages(run_main, return_data_check_before_detail, onlineData, 
-                               online_stores, goods_art_no_arrays, handler_result_folder):
+                               online_stores, goods_art_no_arrays, handler_result_folder,request_params):
     """处理详情页生成和上传"""
     check_for_detail_first_res = run_main.check_for_detail_first_call_back(
-        return_data_check_before_detail
+        return_data_check_before_detail,request_params
     )
     print("<======>check_for_detail_first_res<======>",check_for_detail_first_res)
     if isinstance(check_for_detail_first_res, partial):
-        result = check_for_detail_first_res()
         try:
+            result = check_for_detail_first_res()
             config_data = result["config_data"]
         except:
             config_data = result
@@ -984,7 +1002,7 @@ def device_config_detail(params: ModelGetDeviceConfigDetail):
     model = configModel.read(session, conditions={"id": action_id})
     session.close()
     if model == None:
-        return {"code": 1, "msg": "数据不存在", "data": None}
+        return {"code": 1, "msg": "相关配置不存在,请删除当前货号后重新拍摄", "data": None}
     return {"code": 0, "msg": "", "data": model}
 
 
@@ -1070,45 +1088,86 @@ def reset_config(params: ModelGetDeviceConfig):
 @app.get("/get_photo_records", description="获取拍照记录")
 def get_photo_records(page: int = 1, size: int = 5):
     session = SqlQuery()
+    current_page = page
     # photos = CRUD(PhotoRecord)
     print("准备查询拍摄记录", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
-    statement = (
-        select(PhotoRecord)
+    
+    # 首先统计总数
+    count_statement = (
+        select(func.count(PhotoRecord.goods_art_no.distinct()))
+        .where(PhotoRecord.delete_time == None)
+    )
+    total_count = session.exec(count_statement).one()
+    
+    # 查询所有不重复的货号及对应的最大时间,进行分页
+    base_statement = (
+        select(PhotoRecord.goods_art_no, func.max(PhotoRecord.id).label('max_id'))
+        .where(PhotoRecord.delete_time == None)
+        .group_by(PhotoRecord.goods_art_no)
+        .order_by(desc('max_id'))
         .offset((page - 1) * size)
         .limit(size)
-        .order_by(desc("id"))
-        .group_by("goods_art_no")
     )
-    list = []
-    result = session.exec(statement).all()
-    print("group 完成 ", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
-    join_conditions = [
-        {
-            "model": DeviceConfig,
-            "on": PhotoRecord.action_id == DeviceConfig.id,
-            "is_outer": False,  # 可选,默认False,设为True则为LEFT JOIN
-        }
-    ]
-    for item in result:
+    paginated_results = session.exec(base_statement).all()
+    
+    # 获取这些货号的详细记录
+    list_data = []
+    if paginated_results:
+        # 获取当前页的货号列表
+        current_goods_art_nos = [item.goods_art_no for item in paginated_results]
+        
+        # 查询这些货号的所有记录
         query = (
             select(PhotoRecord, DeviceConfig.action_name)
-            .where(PhotoRecord.goods_art_no == item.goods_art_no)
-            .join(DeviceConfig, PhotoRecord.action_id == DeviceConfig.id)
-        )
-        list_item = session.exec(query).mappings().all()
-        list.append(
-            {
-                "goods_art_no": item.goods_art_no,
-                "action_time": item.create_time,
-                "items": list_item,
-            }
+            .outerjoin(DeviceConfig, PhotoRecord.action_id == DeviceConfig.id)
+            .where(PhotoRecord.goods_art_no.in_(current_goods_art_nos))
+            .where(PhotoRecord.delete_time == None)
+            .order_by(asc("image_index"))  # 按货号分组并按ID倒序
         )
+        all_items = session.exec(query).mappings().all()
+        
+        # 按货号分组
+        items_by_goods = {}
+        for item in all_items:
+            goods_art_no = item.PhotoRecord.goods_art_no
+            if goods_art_no not in items_by_goods:
+                items_by_goods[goods_art_no] = []
+            items_by_goods[goods_art_no].append(item)
+        
+        # 构建结果列表,保持分页的顺序
+        for item in paginated_results:
+            goods_art_no = item.goods_art_no
+            if goods_art_no in items_by_goods:
+                # 获取该货号下时间最新的记录作为action_time
+                latest_record = items_by_goods[goods_art_no][0].PhotoRecord
+                list_data.append(
+                    {
+                        "goods_art_no": goods_art_no,
+                        "action_time": latest_record.create_time,
+                        "items": items_by_goods[goods_art_no],
+                    }
+                )
+    
     session.close()
     print("循环查询 完成 ", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+    
+    # 计算分页信息
+    total_pages = (total_count + size - 1) // size  # 向上取整
+    has_prev = page > 1
+    has_next = page < total_pages
+    
     return {
         "code": 0,
         "msg": "",
-        "data": {"list": list, "page": page, "size": size},
+        "data": {
+            "list": list_data, 
+            "current_page": current_page,
+            "size": size,
+            "total_count": total_count,
+            "total_pages": total_pages,
+            "has_prev": has_prev,
+            "has_next": has_next
+        },
     }
 
 
@@ -1118,6 +1177,7 @@ def get_last_photo_record():
     statement = (
         select(PhotoRecord)
         .where(PhotoRecord.image_path != None)
+        .where(PhotoRecord.delete_time == None)
         .order_by(desc("photo_create_time"))
     )
     result = session.exec(statement).first()
@@ -1135,7 +1195,7 @@ def get_photo_record_detail(goods_art_no: str = None):
         return {"code": 1, "msg": "参数错误", "data": None}
     session = SqlQuery()
     photos = CRUD(PhotoRecord)
-    items = photos.read_all(session, conditions={"goods_art_no": goods_art_no})
+    items = photos.read_all(session, conditions={"goods_art_no": goods_art_no,"delete_time": None})
     session.close()
     return {
         "code": 0,
@@ -1153,7 +1213,8 @@ def delect_goods_arts(params: PhotoRecordDelete):
     session = SqlQuery()
     photos = CRUD(PhotoRecord)
     for item in params.goods_art_nos:
-        photos.deleteConditions(session, conditions={"goods_art_no": item})
+        settings.syncPhotoRecord({"goods_art_no": item},action_type=2)
+        photos.deleteConditions(session, conditions={"goods_art_no": item,"delete_time": None})
     session.close()
     return {
         "code": 0,
@@ -1233,7 +1294,7 @@ def update_left_right_config(params: LeftRightParams):
 def update_record(params: RecordUpdate):
     session = SqlQuery()
     photoRecord = CRUD(PhotoRecord)
-    model = photoRecord.read(session, conditions={"id": params.id})
+    model = photoRecord.read(session, conditions={"id": params.id,"delete_time": None})
     if model == None:
         return {"code": 1, "msg": "记录不存在", "data": None}
     kwargs = params.__dict__
@@ -1377,12 +1438,15 @@ def syncUserJsonConfigs(token):
 @app.post("/sync_sys_configs", description="同步线上配置到本地")
 def sync_sys_configs(params: SyncLocalConfigs):
     hlm_token = params.token
+    env = params.env
+    settings.USER_TOKEN = hlm_token
+    settings.USER_ENV = env
     headers = {
         "Authorization": f"Bearer {hlm_token}",
         "content-type": "application/json",
     }
     # 追加配置参数 machine_type 拍照机设备类型;0鞋;1服装
-    url = settings.DOMAIN + f"/api/ai_image/camera_machine/get_all_user_configs?machine_type={MACHINE_TYPE}"
+    url = settings.getDoman(env) + f"/api/ai_image/camera_machine/get_all_user_configs?machine_type={MACHINE_TYPE}"
     result = requests.get(url=url, headers=headers)
     print("result",result.json())
     sys_configs = result.json().get("data", {}).get("configs")
@@ -1416,11 +1480,14 @@ def sync_sys_configs(params: SyncLocalConfigs):
 @app.post("/sync_actions", description="同步左右脚配置到本地")
 def sync_action_configs(params: SyncLocalConfigs):
     hlm_token = params.token
+    env = params.env
+    settings.USER_TOKEN = hlm_token
+    settings.USER_ENV = env
     headers = {
         "Authorization": f"Bearer {hlm_token}",
         "content-type": "application/json",
     }
-    url = settings.DOMAIN + f"/api/ai_image/camera_machine/get_all_user_tabs?machine_type={MACHINE_TYPE}"
+    url = settings.getDoman(env) + f"/api/ai_image/camera_machine/get_all_user_tabs?machine_type={MACHINE_TYPE}"
     result = requests.get(url=url, headers=headers)
     session = SqlQuery()
     deviceConfigs = CRUD(DeviceConfig)
@@ -1430,7 +1497,7 @@ def sync_action_configs(params: SyncLocalConfigs):
     if tabs:
         # 先删除再创建
         deviceConfigTabs.deleteConditions(session, {})
-        deviceConfigs.deleteConditions(session, {})
+        deviceConfigs.deleteConditions(session, {},False)
         batch_insert_device_configsNew(session, tabs, actions)
     else:
         all_actions = deviceConfigs.read_all(session)
@@ -1462,3 +1529,304 @@ def sync_action_configs(params: SyncLocalConfigs):
     # syncUserJsonConfigs(hlm_token)
     session.close()
     return {"code": 0, "msg": "操作成功", "data": None}
+
+@app.get("/get_goods_image_json", description="关闭窗口")
+def get_goods_image_json(goods_art_no: str,token:str):
+    remove_pic_ins = RemoveBgALi()
+    if goods_art_no == None or goods_art_no == "":
+                # 判断货号是否存在
+        raise UnicornException("货号不能为空")
+    session = SqlQuery()
+    photoRecord = CRUD(PhotoRecord)
+    goods_art_record = photoRecord.read_all(
+        session, conditions={"goods_art_no": goods_art_no,"delete_time": None}
+    )
+    if not goods_art_record:
+        raise UnicornException("该货号拍摄记录不存在")
+    action_id_array = [record.action_id for record in goods_art_record]
+    devices = CRUD(DeviceConfig)
+    devices_record = devices.read_all(session,conditions={"id": action_id_array})
+    # 提取 action_name 字段并拼接
+    action_names = [str(record.action_name) for record in devices_record]
+    action_names_str = ",".join(action_names)
+    image_arrays = []
+    for goods_art_record_item in goods_art_record:
+        image_path = goods_art_record_item.image_path
+        try:
+            image_url = uploadImage(remove_pic_ins=remove_pic_ins,token=token, local_path=image_path)
+        except Exception as e:
+            raise UnicornException("网络异常,请重试")
+        image_arrays.append(image_url)
+    session.close()
+    return {"code": 0, "msg": "关闭失败", "data": {"customer_template_images": image_arrays,"template_image_order":action_names_str}}
+def uploadImage(remove_pic_ins,token:str, local_path: str) -> str:
+        im = remove_pic_ins.get_image_cut_new(file_path=local_path)
+        post_headers = {"Authorization": "Bearer " +token}
+        url = settings.DOMAIN + "/api/upload"
+        resultData = requests.post(
+            url, files={"file":im}, headers=post_headers
+        ).json()
+        return resultData["data"]["url"]
+    
+    
+    
+@app.post("/remove_background", description="图片抠图-http请求")
+async def remove_background(params:PhotoRecordRemoveBackground):
+    # await socket_manager.send_message(msg="测试")
+    executor = ThreadPoolExecutor(max_workers=4)
+    obj = None
+    token = params.token
+    token = "Bearer " + token
+    uuid = mine_uuid.uuid4().hex
+    run_main = RunMain(obj, token, uuid)
+    goods_art_no_arrays = params.goods_art_nos
+    limit_path = "{}/{}".format(settings.OUTPUT_DIR,
+        time.strftime("%Y-%m-%d", time.localtime(time.time()))
+    )
+    try:
+        move_folder_array = handlerFolderDelete(limit_path,goods_art_no_arrays,True)
+    except UnicornException as e:
+        raise UnicornException(e.msg)
+    # 该数组表示是否需要后面的移动文件夹操作,减少重复抠图,提升抠图时间和速度
+    session = SqlQuery()
+    for goods_art_no in goods_art_no_arrays:
+        pr = CRUD(PhotoRecord)
+        images = pr.read_all(session, conditions={"goods_art_no": goods_art_no,"delete_time": None})
+        if not images:
+            raise UnicornException(f"商品货号【{goods_art_no}】在商品档案资料中不存在,请检查货号是否正确")
+        if move_folder_array.get(goods_art_no) == None:
+            image_dir = "{}/data/".format(os.getcwd()).replace("\\", "/")
+            check_path(image_dir)
+            for idx, itemImg in enumerate(images):
+                if itemImg.image_path == "" or itemImg.image_path == None:
+                    raise UnicornException(f"货号【{goods_art_no}】存在没有拍摄完成的图片,请重拍或删除后重试")
+                new_file_name = (
+                    str(idx)+"_"+str(itemImg.goods_art_no) + "_" + str(idx) + ".jpg"
+                )
+                if not os.path.exists(
+                    image_dir + "/" + os.path.basename(new_file_name)
+                ):
+                    shutil.copy(itemImg.image_path, image_dir + new_file_name)
+            dealImage = DealImage(image_dir)
+            resFlag, path = dealImage.dealMoveImage(
+                image_dir=image_dir,
+                callback_func=None,
+                goods_art_no=goods_art_no,
+            )
+            if not resFlag:
+                raise UnicornException(f"抠图操作异常,请检查目录是否存在,或者权限不足")
+    session.close()
+    # try:
+    cutOutMode = (
+        "1"
+        if settings.getSysConfigs("other_configs", "cutout_mode", "普通抠图")
+        == "普通抠图"
+        else "2"
+    )
+    config_data = {
+        "image_dir": limit_path,
+        "image_order": (
+            "俯视,侧视,后跟,鞋底,内里,组合,组合2,组合3,组合4,组合5,组合6,组合7,组合8,组合9,组合10,组合11,组合12,组合13,组合14,组合15,组合16,组合17,组合18,组合19,组合20,组合21,组合22,组合23,组合24,组合25,组合26"
+        ),
+        "goods_art_no": "",
+        "goods_art_nos": goods_art_no_arrays,
+        "is_check_number": False,
+        "resize_image_view": "后跟",
+        "cutout_mode": cutOutMode,
+        "logo_path": "",
+        "special_goods_art_no_folder_line": "",
+        "is_use_excel": False,  # 是否使用excel
+        "excel_path": "",  # excel路径
+        "is_check_color_is_all": False,
+        "cutout_is_pass": True,
+        "assigned_page_dict": {},
+        "detail_is_pass": True,
+        "upload_is_pass": False,
+        "upload_is_enable": settings.IS_UPLOAD_HLM,  # 是否上传到惠利玛商品库,通过config.ini得is_upload开启
+        "is_filter": False,
+        "temp_class": {},
+        "temp_name": "",
+        "temp_name_list": [],
+        "target_error_folder": f"{limit_path}/软件-生成详情错误",
+        "success_handler": [],
+    }
+    try:
+        loop = asyncio.get_event_loop()
+        return_data = await loop.run_in_executor(
+            executor, partial(run_main.check_before_cutout, config_data)
+        )
+        cutout_res = await loop.run_in_executor(
+            executor,
+            partial(run_main.check_for_cutout_image_first_call_back, return_data),
+        )
+        handler_result = []
+        have_handler_keys = move_folder_array.keys()
+        if cutout_res:
+            handler_result_folder = f"{config_data['image_dir']}"
+            for goods_art_item in goods_art_no_arrays:
+                handler_result.append({
+                    "goods_art_no": goods_art_item,
+                    "success": True,
+                    "info": "处理成功",
+                })
+        else:
+            return {"code": 1, "message": "抠图失败", "data": None} 
+        if len(have_handler_keys) == len(goods_art_no_arrays) or (len(have_handler_keys) == 0  and cutout_res):
+            handler_result_folder = handler_result_folder.replace("\\", "/")
+            success_items = [item for item in handler_result if item.get('success') == True]
+            cutout_folder = handler_result_folder+"/"+success_items[0].get("goods_art_no")+"/800x800" if len(success_items) > 0 else ""
+            progress = {
+                "status": "处理完成",
+                "current": len(goods_art_no_arrays),
+                "total": len(goods_art_no_arrays),
+                "error": 0,
+                "folder":cutout_folder,
+            }
+            return {"code": 0, "message": "抠图完成", "data": {"output_folder": handler_result_folder, "list": handler_result,"progress":progress}}
+    except UnicornException as e:
+        raise UnicornException(e.msg)
+    except Exception as e:
+        print("error",e)
+        raise UnicornException(f"抠图异常,请稍后重试:{e}")
+
+
+@app.post("/syncPhotoRecord", description="同步本地拍照记录-和output目录")
+async def syncPhotoRecord(params:SyncPhotoRecord):
+    # 查询所有不重复的货号及对应的最大时间,进行分页
+    settings.USER_TOKEN = params.token
+    settings.USER_ENV = params.env
+    syncStatus = settings.checkRecordSyncStatus()
+    if syncStatus == True:
+        # 同步过就无需再同步
+        return {"code": 0, "message": "同步完成", "data": None}
+    session = SqlQuery()
+    base_statement = (
+        select(PhotoRecord)
+        .where(PhotoRecord.delete_time == None)
+    )
+    paginated_results = session.exec(base_statement).all()
+    
+    def model_to_dict(model):
+        """将SQLAlchemy模型对象转换为字典,排除_sa_instance_state"""
+        result = {}
+        for column in model.__table__.columns:
+            result[column.name] = getattr(model, column.name)
+        return result
+    
+    json_results = []
+    for result in paginated_results:
+        # 使用自定义函数将SQLAlchemy对象转换为字典
+        json_results.append(model_to_dict(result))
+    # 最终转换为JSON字符串
+    settings.syncPhotoRecord(json_results,action_type=0)
+    session.close()
+    return {"code": 0, "message": "同步完成", "data": None}
+def copy_directory_walk(src, dst):
+    """
+    使用 os.walk() 遍历并复制目录
+    """
+    for root, dirs, files in os.walk(src):
+        # 计算相对路径
+        rel_path = os.path.relpath(root, src)
+        dst_dir = os.path.join(dst, rel_path) if rel_path != '.' else dst
+        # 创建目标目录
+        if not os.path.exists(dst_dir):
+            os.makedirs(dst_dir)
+        # 复制文件
+        for file in files:
+            src_file = os.path.join(root, file)
+            dst_file = os.path.join(dst_dir, file)
+            shutil.copy2(src_file, dst_file)
+def rename_file_safe(src, dst, overwrite=True):
+    """
+    安全地重命名文件
+    
+    Args:
+        src: 源文件路径
+        dst: 新文件路径
+        overwrite: 是否覆盖已存在的文件
+    """
+    try:
+        # 检查源文件是否存在
+        if not os.path.exists(src):
+            return False, f"源文件 {src} 不存在"
+        
+        # 检查目标文件是否存在
+        if os.path.exists(dst) and not overwrite:
+            return False, f"目标文件 {dst} 已存在,设置 overwrite=True 以覆盖"
+        
+        # 执行重命名
+        os.rename(src, dst)
+        return True, f"文件已成功从 {src} 重命名为 {dst}"
+    except OSError as e:
+        return False, f"重命名失败: {str(e)}"
+@app.post("/rename_shadow_folder", description="同步本地拍照记录-和output目录")
+async def rename_shadow_folder(params:RenameShadow):
+    # 货号数组
+    goods_art_nos = params.goods_art_nos
+    # for goods_art_no in goods_art_nos:
+    # 查询这些货号的所有记录
+    goods_art_dict = {}
+    for goods in goods_art_nos:
+        query = (
+            select(PhotoRecord, DeviceConfig.action_name)
+            .outerjoin(DeviceConfig, PhotoRecord.action_id == DeviceConfig.id)
+            .where(PhotoRecord.goods_art_no.in_(goods_art_nos))
+            .where(PhotoRecord.delete_time == None)
+            .order_by(PhotoRecord.goods_art_no, asc("id"))  # 按货号分组并按ID倒序
+        )
+        all_items = session.exec(query).mappings().all()
+        if len(all_items) == 0:  # 如果没有记录则返回
+            # raise UnicornException("暂无可用货号")
+            continue
+        goods_art_rename_list = []
+        for item in all_items:
+            if item["action_name"] == None:
+                continue
+            data = {"goods_art_no": item.PhotoRecord.goods_art_no, "action_name": item["action_name"],"image_index":item.PhotoRecord.image_index}
+            goods_art_rename_list.append(data)
+        goods_art_dict[goods] = goods_art_rename_list
+    outputDir = settings.OUTPUT_DIR
+    if not os.path.exists(outputDir):
+        raise UnicornException(f"生成目录[{outputDir}]暂无文件,请拍摄或抠图后处理")
+    outputList = os.listdir(outputDir)
+    success_result = []
+    for firstDir in outputList:
+        secondPath = f"{outputDir}\\{firstDir}"
+        for goodsArt in goods_art_dict:
+            goods_art_no_obj = goods_art_dict[goodsArt]
+            goodsArtPath = f"{secondPath}\\{goodsArt}"
+            if not os.path.exists(goodsArtPath):
+                continue
+            renameSrcPath = f"{goodsArtPath}\\阴影图处理"
+            renameDstPath = f"{goodsArtPath}\\阴影图处理-重命名"
+            if not os.path.exists(renameSrcPath):
+                print("阴影图目录不存在...","不处理")
+                continue
+            if len(os.listdir(renameSrcPath)) == 0:
+                print("阴影图处理目录无内容...","不处理")
+                continue
+            if not os.path.exists(renameDstPath):
+                try:
+                    copy_directory_walk(renameSrcPath,renameDstPath)
+                except Exception as e:
+                    print("重命名失败",e)
+                    continue
+            dstPath = os.listdir(renameDstPath)
+            for dts_files in dstPath:
+                for goods_obj in goods_art_no_obj:
+                    goods_art_no_name = goods_obj["goods_art_no"]
+                    image_index = goods_obj["image_index"]
+                    action_name = goods_obj["action_name"]
+                    image_index+=1
+                    basic_name = f"{goods_art_no_name}({image_index})"
+                    if basic_name in dts_files:
+                        if "抠图" in dts_files:
+                            is_ok,msg =  rename_file_safe(f"{renameDstPath}\\{dts_files}",f"{renameDstPath}\\{basic_name}_{action_name}_抠图.png")
+                            print(is_ok,msg)
+                        if "阴影" in dts_files:
+                            is_ok,msg = rename_file_safe(f"{renameDstPath}\\{dts_files}",f"{renameDstPath}\\{basic_name}_{action_name}_阴影.png")
+                            print(is_ok,msg)
+                        print("goods_obj",goods_obj)
+            success_result.append({"goods_art_no":goodsArt,"path":renameDstPath})
+    return {"code": 0, "message": "重命名完成", "data": {"result": success_result}}

Diferenças do arquivo suprimidas por serem muito extensas
+ 18 - 0
python/canvas_json.json


+ 4 - 1
python/config.ini

@@ -47,4 +47,7 @@ stop = 9
 ; low_iso = 100
 ; high_iso = 6400
 [output_config]
-output_dir = ..\..\..\output\
+output_dir = ..\..\..\output\
+
+[customer_template]
+template_url = http://localhost:3001

+ 1 - 0
python/custom_plugins/plugins_mode/detail_generate_base.py

@@ -390,6 +390,7 @@ class DetailBase(object):
 
     def create_folder(self, path):
         if not os.path.exists(path):
+            print(f"创建目录   详情页--系统---=================>>>>:{path}")
             os.makedirs(path)
 
     def get_all_process_pics(self):

+ 33 - 6
python/databases.py

@@ -4,7 +4,8 @@ from typing import Dict
 from datetime import datetime
 from typing import Optional
 import json
-from sqlalchemy import and_, desc, asc, delete
+from sqlalchemy import and_, desc, asc, delete,inspect
+import settings
 from utils.utils_func import check_path
 from sqlalchemy.dialects import sqlite
 from model import DeviceConfig, PhotoRecord, SysConfigs, DeviceConfigTabs
@@ -139,10 +140,13 @@ class CRUD:
         self,
         session: Session,
         conditions: Optional[Dict] = None,
+        is_soft_delete: bool = True,
     ):
         query = select(self.model)
-        if conditions == None:
+        if conditions is None:
             return False
+        
+        # 构建查询条件
         query = query.where(
             and_(
                 *(
@@ -151,11 +155,25 @@ class CRUD:
                 )
             )
         )
+        
+        # 获取需要删除的对象
         objects_to_delete = session.exec(query).all()
-        for obj in objects_to_delete:
-            session.delete(obj)
-        session.commit()
-        # session.refresh()
+        
+        # 检查模型是否包含 delete_time 字段
+        model_columns = {column.name for column in inspect(self.model).columns}
+        if 'delete_time' in model_columns and is_soft_delete ==True:
+            # 软删除:更新 delete_time 字段
+            for obj in objects_to_delete:
+                setattr(obj, 'delete_time', datetime.now())
+            session.commit()
+            print("软删除完成")
+        else:
+            # 硬删除:直接删除对象
+            for obj in objects_to_delete:
+                session.delete(obj)
+            session.commit()
+            print("硬删除完成")
+        
         return True
 
     def delete(self, session: Session, obj_id: int):
@@ -286,6 +304,15 @@ async def insert_photo_records(
         session.commit()
         session.refresh(device_config)
         record_id = device_config.id
+        syncData = {
+            "id": record_id,
+            "image_deal_mode": image_deal_mode,
+            "goods_art_no": goods_art_no,
+            "image_index": image_index,
+            "action_id": action_id,
+        }
+        # 异步插入一条数据
+        settings.syncPhotoRecord(syncData,action_type=1)
         return True, record_id
 
 

+ 18 - 16
python/detail_template_test_xinnuo.json

@@ -7,23 +7,25 @@
                 "文件夹名称": "AC51016112",
                 "编号": "AC51016112",
                 "颜色名称": "枪色",
+                "模特图": "C:/Development/project/output/2025-12-05/详情图-测试/模特图.jpg",
+                "场景图": "C:/Development/project/output/2025-12-05/详情图-测试/场景图.jpg",
                 "pics": {
-                   "俯视-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(1)_俯视_抠图.png",
-                "俯视-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(1)_俯视_阴影.png",
-                "侧视-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(2)_侧视_抠图.png",
-                "侧视-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(2)_侧视_阴影.png",
-                "后跟-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(3)_后跟_抠图.png",
-                "后跟-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(3)_后跟_阴影.png",
-                "鞋底-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(4)_鞋底_抠图.png",
-                "鞋底-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(4)_鞋底_阴影.png",
-                "内里-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(5)_内里_抠图.png",
-                "内里-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(5)_内里_阴影.png",
-                "组合-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(6)_组合_抠图.png",
-                "组合-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(6)_组合_阴影.png",
-                "组合2-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(7)_组合2_抠图.png",
-                "组合2-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(7)_组合2_阴影.png",
-                "组合3-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(8)_组合3_抠图.png",
-                "组合3-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(8)_组合3_阴影.png"
+                    "俯视-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(1)_俯视_抠图.png",
+                    "俯视-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(1)_俯视_阴影.png",
+                    "侧视-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(2)_侧视_抠图.png",
+                    "侧视-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(2)_侧视_阴影.png",
+                    "后跟-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(3)_后跟_抠图.png",
+                    "后跟-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(3)_后跟_阴影.png",
+                    "鞋底-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(4)_鞋底_抠图.png",
+                    "鞋底-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(4)_鞋底_阴影.png",
+                    "内里-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(5)_内里_抠图.png",
+                    "内里-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(5)_内里_阴影.png",
+                    "组合-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(6)_组合_抠图.png",
+                    "组合-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(6)_组合_阴影.png",
+                    "组合2-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(7)_组合2_抠图.png",
+                    "组合2-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(7)_组合2_阴影.png",
+                    "组合3-抠图": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(8)_组合3_抠图.png",
+                    "组合3-阴影": "C:/Users/15001/Desktop/2025-06-11/A333/阴影图处理/A333(8)_组合3_阴影.png"
                 },
                 "800x800": [
                     "C:/Users/15001/Desktop/测试文件夹/AC51016112/800x800/AC51016112(1).jpg",

+ 35 - 0
python/docs/socket命令.md

@@ -1108,4 +1108,39 @@ _(该命令用于单独自定义配置中某一项的单独调整测试,不进
     "msg_type": "segment_progress"
 }
 ```
+##### 发送获取设备状态命令
+* data:null
+* msg_type:固定为[get_mcu_info]
+```
+{
+    "type": "get_mcu_info",
+    "data": null
+}
+```
+###### 响应示例-成功
+* state_camera_motor:相机高度状态
+* state_camera_steering:相机角度状态
+* state_turntable_steering:转盘状态
+* state_move_turntable_steering:转盘前后移动状态
+* state_overturn_steering:翻板状态
+```
+{
+    "code": 0,
+    "msg": "获取mcu设备运行状态信息",
+    "status": 2,
+    "data": {
+        "_type": "show_mcu_info",
+        "plugins_mode": "mcu",
+        "data": "激光状态;1,相机高度状态:2,相机角度状态:2,转盘状态:2,转盘前后移动状态:2,翻板状态:2,flag:52",
+        "data_state": {
+            "state_camera_motor": 2,
+            "state_camera_steering": 2,
+            "state_turntable_steering": 2,
+            "state_overturn_steering": 2,
+            "state_move_turntable_steering": 2
+        }
+    },
+    "msg_type": "get_mcu_info"
+}
+```
 ##### 未完待续.....

+ 97 - 49
python/mcu/DeviceControl.py

@@ -47,6 +47,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         self.state_camera_steering = 3
         self.state_turntable_steering = 3
         self.state_overturn_steering = 3
+        self.last_camera_height = 0
         # 是否实时获取mcu状态信息
         self.is_get_mcu_state = True
         self.state_move_turntable_steering = 3
@@ -78,7 +79,19 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             "mp3_player": 8,
             "mcu": 99,
         }
-
+        self.last_move_time = time.time()
+        self.device_name_dict_mapping = {
+            0:"相机角度",
+            1:"相机高度",
+            2:"转盘角度",
+            3:"翻板角度",
+            4:"激光灯位置",
+            5:"蜂鸣器",
+            6:"split",
+            7:"转盘位置",
+            8:"播放音频",
+            99:"mcu命令",
+        }
         # 最近的mcu基础信息,用于获取数据状态检查
         self.last_mcu_info_data = {
             "num": 0,
@@ -127,6 +140,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             90: self.get_from_mcu_connect_info,  # 获取链接电脑信号
             92: self.get_from_mcu_move_respond_data,  # 获取MCU响应
             100: self.print_mcu_error_data,  # 打印下位机的错误内容
+            255: self.print_mcu_noraml_data,  # 打印回执
         }
 
     async def sendCommand(self, command):
@@ -194,24 +208,14 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         self.to_init_device_origin_point(device_name="mcu", is_force=is_force)
         print("MCU 开始循环~")
         logger.info("MCU 开始循环~")
-        loop = asyncio.get_event_loop()
         while 1:
-            await asyncio.sleep(1)
+            await asyncio.sleep(0.05)
             if not self.serial_ins or not self.connect_state:
                 break
             try:
-                # self.sendSocketMessage(
-                # 0,
-                #     msg="mcu循环监听中",
-                #     data={},
-                #     device_status=2,
-                # )
-                # print("mcu   send_cmd")
-                loop.create_task(self.send_cmd())
-                # time.sleep(0.01)
+                self.send_cmd()
                 if not self.get_basic_info_mcu():
                     pass
-                # self.close_other_window()
             except BaseException as e:
                 print("121231298908", e)
                 logger.info("121231298908", e)
@@ -414,19 +418,20 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         self.lock.release()
 
     async def send_all_cmd(self):
-        await asyncio.sleep(0.001)
+        # await asyncio.sleep(0.001)
         while True:
+            await asyncio.sleep(0.1)
             if self.send_data_queue:
                 # self.sendSocketMessage(msg="正在发送命令", device_status=1)
                 data = self.send_data_queue.pop(0)
+                # print("\033[1;32;40m 正在发送命令 \033[0m",data)
                 self.serial_ins.write_cmd(data)
                 # self.sendSocketMessage(msg="命令发送完成", device_status=2)
             else:
                 break
 
-    async def send_cmd(self):
+    def send_cmd(self):
         self.lock.acquire()
-        asyncio.sleep(0.01)
         if self.send_data_queue:
             # self.sendSocketMessage(msg="正在发送命令", device_status=1)
             data = self.send_data_queue.pop(0)
@@ -453,14 +458,55 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             data = receive_data[1:].decode()
             if "设备初始化完成" in data:
                 self.init_state = True
+                logger.info("设备初始化完成:%s", data)
                 self.sendSocketMessage(msg=data, device_status=2)
-            print("115  print_mcu_error_data:", data)
-            logger.info("115  print_mcu_error_data:%s", data)
+            else:
+                print("设备异常数据打印:", data)
+                logger.info("115  设备异常数据打印:%s", data)
         except BaseException as e:
             print("117 error {}".format(e))
             logger.info("117 error %s", e)
         return
-
+    def print_mcu_noraml_data(self, receive_data):
+        # 扫码数据
+        print("接收到255数据:", receive_data)
+        try:
+            command_mapping = {
+                1:"设备运动",
+                2:"初始化设备",  # 初始化设备
+                3:"处理其他设备",  # 处理其他设备
+                29:"获取所有信息",  # 获取所有信息
+                40:"设置偏移量",  # 设置偏移量
+                41:"读取偏移量",  # 读取偏移量
+                91:"信号转发处理",  # 信号转发处理
+                92:"信号转发返回",  # 信号转发返回
+                44:"获取其他信息",  # 获取其他信息
+                43:"RGB灯的处理与通讯",  ## RGB灯的处理与通讯
+                45:"设置其他信息",  # 设置其他信息
+                47:"查询遥控器电量",  # 查询遥控器电量
+                48:"设置转盘通讯方式 1、串口、2、无线、3 混合",  # 
+                93:"停止运行mcu",  # 停止运行mcu
+                90:"连接MCU",# 连接MCU
+            }
+            # command = int(receive_data[0])
+            command = int(receive_data[1])
+            command_text = command_mapping[command]
+            # receive_data_temp = receive_data[2:]
+            receive_data_temp_text = " ".join([hex(x) for x in receive_data])
+            # print("255  command_text:", command_text)
+            if command_text in ["设备运动","处理其他设备"]:
+                device_id = int(receive_data[2])
+                device_value = int(receive_data[3])
+                device_name_info = self.device_name_dict_mapping[device_id]
+                message_info = {"设备名称":device_name_info,"运动值":device_value}
+                print("【设备运动】消息回执:", message_info)
+                logger.info("设备运动消息回执:%s", message_info)
+            print("接收设备消息回执:", command_text)
+            logger.info("接收设备消息回执:%s", receive_data_temp_text)
+        except BaseException as e:
+            print("255 error {}".format(e))
+            logger.info("255 error %s", e)
+        return
     def get_from_mcu_move_respond_data(self, receive_data):
         self.last_from_mcu_move_respond_data = receive_data
 
@@ -556,14 +602,8 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             return False
         if not receive_data:
             return False
-        # print("receive_data", receive_data)
-        # 数据 结构 command,按命令解析
-        # command 0(9) 相机高度1-2  相机角度3-4  转盘角度5-6 灯光状态7  激光指示器状态8,运行状态9
         command = receive_data[0]
-        # receive_data_temp = receive_data[2:]
-        # receive_data_temp_text = " ".join([hex(x) for x in receive_data_temp])
-        # print("command",command)
-        # print("receive_data", receive_data_temp_text)
+        print("get_basic_info_mcu",command)
         if command in self.deal_code_func_dict:
             self.deal_code_func_dict[command](receive_data)
 
@@ -863,7 +903,9 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                     "state_overturn_steering": self.state_overturn_steering,
                 },
             }
+            self.msg_type = "get_mcu_info"
             self.sendSocketMessage(msg="获取mcu设备运行状态信息", data=message)
+            self.msg_type = "mcu"
             # print("转盘:{},时间:{}".format(self.state_turntable_steering, time.time()))
 
         if len(receive_data) == 8:
@@ -907,7 +949,9 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             ):
                 self.init_state = True
                 self.sendSocketMessage(msg="设备初始化完成", device_status=2)
+            self.msg_type = "get_mcu_info"
             self.sendSocketMessage(msg="获取mcu设备运行状态信息", data=message)
+            self.msg_type = "mcu"
         # 检查是否成功初始化
         if self.is_just_init_time is False:
             if self.mcu_move_state == 2:
@@ -1041,8 +1085,8 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
 
         await asyncio.sleep(0.3)
         receive_data = self.read_cmd(serial_handle)
-        print("尝试接收命令", receive_data)
-        logger.info("尝试接收命令,%s", receive_data)
+        # print("尝试接收命令", receive_data)
+        # logger.info("尝试接收命令,%s", receive_data)
         device_id = 0
 
         if receive_data:
@@ -1086,8 +1130,8 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             elif device_id == 2:
                 # 有线接收器
                 self.connected_ports_dict[port_name] = "Line_Control"
-                print("device_id", device_id)
-                logger.info("device_id %s", device_id)
+                # print("device_id", device_id)
+                # logger.info("device_id %s", device_id)
                 state = await self.line_control.to_connect_com(port_name)
                 if not state:
                     return False
@@ -1317,7 +1361,6 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         print("移动", time.time())
         logger.info("移动,%s", time.time())
         speed = settings.moveSpeed()
-
         cmd = 1
         device_id = self.device_name_dict[device_name]
         # if device_id != 1:
@@ -1343,7 +1386,6 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                     if down_speed is None
                     else down_speed
                 )
-
                 value = value / 10  # value 单位毫米
                 assert 0 <= value <= 40
                 assert 0 <= max_speed <= 10000
@@ -1410,6 +1452,8 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             is_relative,
         ]
         self.add_send_data_queue(data)
+        current_time = time.time()
+        self.last_move_time = current_time
 
     def to_get_mcu_base_info(self):
         if self.connect_state:
@@ -1467,11 +1511,10 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 self.to_device_move(
                     device_name=device_name,
                     value=float(value),
-                    # max_speed=1400,
-                    # up_speed=400,
-                    # down_speed=100,
-                    # _is_debug=_is_debug,
-                    # is_deviation=is_deviation,
+                )
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
                 )
             case "camera_steering":
                 print(device_name, value)
@@ -1479,27 +1522,30 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 self.to_device_move(
                     device_name=device_name,
                     value=float(value),
-                    # _is_debug=_is_debug,
-                    # is_deviation=0,
+                )
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
                 )
             case "turntable_steering":
                 # 转盘舵机
                 self.to_device_move(
                     device_name=device_name,
                     value=float(value),
-                    # _is_debug=_is_debug,
-                    # is_deviation=0,
+                )
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
                 )
             case "turntable_position_motor":
                 # 转盘舵机
                 self.to_device_move(
                     device_name=device_name,
                     value=float(value),
-                    # max_speed=1400,
-                    # up_speed=400,
-                    # down_speed=100,
-                    # _is_debug=_is_debug,
-                    # is_deviation=is_deviation,
+                )
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
                 )
             case "overturn_steering":
                 # 翻板舵机中位
@@ -1524,8 +1570,6 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 self.to_deal_device(device_name, value=value, _type=0, times=1)
             case _:
                 pass
-            # case "photograph":
-            #     self.photograph(goods_art_no=None)
 
     def checkDevice(self):
         print("检查设备是否运行中")
@@ -1624,7 +1668,9 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                     image_index=image_index,
                     smart_shooter=smart_shooter,
                     record_id=record_id,
+                    is_get_mcu_state=True,
                 )
+                program_item.last_move_time = self.last_move_time
                 if self.action_state != 1:
                     # 异常终止
                     print("action异常终止")
@@ -1699,7 +1745,9 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 image_index=image_index,
                 smart_shooter=smart_shooter,
                 record_id=action_id,
+                is_get_mcu_state=False,
             )
+            program_item.last_move_time = self.last_move_time
             if self.action_state != 1:
                 # 异常终止
                 print("action异常终止")
@@ -1783,7 +1831,7 @@ async def checkMcuConnection(device_ctrl: DeviceControl):
         ports_dict = device_ctrl.scan_serial_port()
         device_ctrl.temp_ports_dict = ports_dict
         # print("device_ctrl.p_list", device_ctrl.p_list)
-        logger.info("device_ctrl.p_list %s", device_ctrl.p_list)
+        # logger.info("device_ctrl.p_list %s", device_ctrl.p_list)
         if not ports_dict:
             # 全部清空 移除所有串口
             if device_ctrl.p_list:

+ 1 - 1
python/mcu/Mcu.py

@@ -655,7 +655,7 @@ class Mcu(BaseClass, metaclass=SingletonType):
         self.to_init_device_origin_point(device_name="mcu")
         print("MCU 开始循环~")
         while 1:
-            time.sleep(0.01)
+            time.sleep(0.1)
             if not self.serial_ins or not self.connect_state:
                 break
             try:

+ 84 - 8
python/mcu/ProgramItem.py

@@ -7,7 +7,9 @@ import settings
 import time
 from .capture.module_digicam import DigiCam
 from .capture.module_watch_dog import FileEventHandler
+import logging
 
+logger = logging.getLogger(__name__)
 
 class ProgramItem(BaseClass):
     # program_sign = Signal(dict)
@@ -22,6 +24,7 @@ class ProgramItem(BaseClass):
         image_index: int = -1,
         record_id: int = -1,
         smart_shooter=None,
+        is_get_mcu_state=True,
     ):
         super().__init__(BaseClass)
         # 1 表示等待中,2表示没有等待
@@ -51,6 +54,7 @@ class ProgramItem(BaseClass):
         self.is_need_confirm = self.get_value(action_data, "is_need_confirm", False)
         self.image_index = self.get_value(action_data, "picture_index", 99)
         self.camera_height = self.get_value(action_data, "camera_height", 0)
+        self.last_camera_height = 0
         self.camera_angle = float(self.get_value(action_data, "camera_angle", 0.0))
         self.af_times = self.get_value(action_data, "number_focus", 0)
         self.shoe_overturn = self.get_value(action_data, "shoe_upturn", False)
@@ -58,6 +62,7 @@ class ProgramItem(BaseClass):
         self.turntable_position = float(
             self.get_value(action_data, "turntable_position", 0.0)
         )
+        self.is_get_mcu_state = is_get_mcu_state
         self.turntable_angle = float(
             self.get_value(action_data, "turntable_angle", 0.0)
         )
@@ -71,7 +76,7 @@ class ProgramItem(BaseClass):
 
         self.set_other()
         self.error_info_text = ""  # 错误提示信息
-
+        self.last_move_time = None
         # self.setParent(parent)
         self.mcu = mcu
 
@@ -173,13 +178,45 @@ class ProgramItem(BaseClass):
             else:
                 self.mcu.to_get_mcu_base_info()
                 asyncio.create_task(self.mcu.send_all_cmd())
+                # self.mcu.send_all_cmd()
                 await asyncio.sleep(0.5)
                 self.mcu.get_basic_info_mcu()
                 # return True
         print("\033[1;31m执行结束\033[0m", self.mcu.action_state)
         # await asyncio.sleep(0.1)
         # self.mcu.to_get_mcu_base_info()
-
+    async def camera_check_mcu_move_is_stop(self, re_check=False):
+        self.error_info_text = ""
+        # 发送基础数据信息
+        # self.mcu.to_get_mcu_base_info()
+        _s = time.time()
+        check_times = 0
+        await self.mcu.cleanAllReceiveData()
+        while 1:
+            if self.mcu.action_state != 1:
+                return False
+            # 发送获取设备状态消息
+            self.mcu.send_get_all_info_to_mcu()
+            await asyncio.sleep(0.5)
+            if all(
+                    value == 2
+                    for value in [
+                        self.mcu.state_camera_motor,
+                        self.mcu.state_camera_steering,
+                        self.mcu.state_turntable_steering,
+                        self.mcu.state_overturn_steering,
+                    ]
+                ):
+                logger.info("拍照前运动检测状态[成功]")
+                await asyncio.sleep(1)
+                return True
+            else:
+                check_times += 1
+                if check_times > 5:
+                    logger.info("拍照前运动检测状态[失败]")
+                    return False
+                # return True
+        print("\033[1;31m执行结束\033[0m", self.mcu.action_state)
     async def run(self, total_len=5, *args):
         if total_len == 1:
             self.mode_type = "其他配置"
@@ -237,7 +274,8 @@ class ProgramItem(BaseClass):
         # if not self.goods_art_no:  # and self.action_name != "初始化位置"
         #     return False
         start_time = time.time()
-        self.mcu.is_get_mcu_state = False
+        current_time = time.time()
+        self.mcu.is_get_mcu_state = self.is_get_mcu_state 
         # ============连接MCU 处理步进电机与舵机等
         if settings.IS_MCU:
             if self.mode_type != "其他配置" and await self.check_mcu_move_is_stop() is False:
@@ -255,32 +293,66 @@ class ProgramItem(BaseClass):
 
             if self.shoe_overturn:
                 self.mcu.to_deal_device(device_name="overturn_steering")
-                # time.sleep(0.1)
+                await asyncio.sleep(0.01)
             if self.camera_height is not None:
+                if (current_time - self.last_move_time)>110:
+                    if self.camera_height == 0:
+                        self.mcu.to_device_move(
+                            device_name="camera_high_motor", value=1
+                        )
+                    elif self.camera_height == 400:
+                        self.mcu.to_device_move(
+                            device_name="camera_high_motor", value=399
+                        )
+                    else:
+                        self.mcu.to_device_move(
+                            device_name="camera_high_motor", value=self.camera_height-1
+                        )
+                    await asyncio.sleep(0.01)
+                    logger.info("设备延迟执行===>,%s",time.time())
                 self.mcu.to_device_move(
                     device_name="camera_high_motor", value=self.camera_height
                 )
-                # time.sleep(0.1)
+                self.last_camera_height = self.camera_height
+                await asyncio.sleep(0.01)
             if self.camera_angle is not None:
                 self.mcu.to_device_move(
                     device_name="camera_steering", value=self.camera_angle
                 )
-                # time.sleep(0.1)
+                await asyncio.sleep(0.01)
 
             if self.turntable_position is not None:
+                if self.turntable_position == 0:
+                        self.mcu.to_device_move(
+                            device_name="turntable_position_motor", value=1
+                        )
+                        logger.info("转盘位置首次运动===>,%s",1)
+                elif self.turntable_position == 800:
+                    self.mcu.to_device_move(
+                        device_name="turntable_position_motor", value=799
+                    )
+                    logger.info("转盘位置首次运动===>,%s",799)
+                else:
+                    self.mcu.to_device_move(
+                        device_name="turntable_position_motor", value=self.turntable_position-1
+                    )
+                    logger.info("转盘位置首次运动===>,%s",self.turntable_position-1)
+                await asyncio.sleep(0.01)
                 self.mcu.to_device_move(
                     device_name="turntable_position_motor",
                     value=self.turntable_position,
                 )
-                # time.sleep(0.1)
+                logger.info("转盘位置2次运动===>,%s",self.turntable_position)
+                await asyncio.sleep(0.01)
 
             if self.turntable_angle is not None:
                 self.mcu.to_device_move(
                     device_name="turntable_steering", value=self.turntable_angle
                 )
-                # time.sleep(0.1)
+                await asyncio.sleep(0.01)
 
             # MCU运动是否有停止检查,设定超时时间
+            # self.mcu.send_all_cmd()
             asyncio.create_task(self.mcu.send_all_cmd())
             if self.mode_type != "其他配置":
                 await asyncio.sleep(1.2)
@@ -288,6 +360,7 @@ class ProgramItem(BaseClass):
                 if await self.check_mcu_move_is_stop(re_check=True) is False:
                     print("MCU检测运动未停止,自动退出,   提前退出")
                     return
+        # logger.info("设备最后一次执行时间===>,%s",current_time)
         self.mcu.is_get_mcu_state = True
         if self.delay_time:
             await asyncio.sleep(self.delay_time)
@@ -295,6 +368,9 @@ class ProgramItem(BaseClass):
             # print("photograph==================")
             self.mcu.to_deal_device(device_name="buzzer", times=1)
             # 用于临时拍照计数
+            if not await self.camera_check_mcu_move_is_stop(re_check=True):
+                logger.info("拍照前运动检测失败===>")
+                return
             is_af = True if self.af_times > 0 else False
             if self.smart_shooter != None:
                 # 拍照

+ 10 - 0
python/mcu/RemoteControlV2.py

@@ -263,6 +263,16 @@ class RemoteControlV2(BaseClass):
         deviceConfigData = deviceConfig.read(
             session=session, conditions={"id": record.action_id}
         )
+        if deviceConfigData == None:
+            self.msg_type = "photo_take"
+            self.sendSocketMessage(
+                code=1,
+                msg="相关配置不存在,请删除当前货号后重新拍摄",
+                data=None,
+                device_status=2,
+            )
+            self.msg_type = "blue_tooth"
+            return
         select_tab_id = deviceConfigData.tab_id
         AllTabConfig = deviceConfig.read_all(
             session=session, conditions={"tab_id": select_tab_id}

+ 2 - 2
python/mcu/SerialIns.py

@@ -69,7 +69,7 @@ class SerialIns(object):
             buf.extend([0xFF & ~sum(data)])
             # 55 55 02 5a 01 a4
             print("send buf  {}".format(self.change_hex_to_int(buf)))
-            logger.info("正在发送命令======>>>>> send buf  %s", self.change_hex_to_int(buf))
+            # logger.info("正在发送命令======>>>>> send buf  %s", self.change_hex_to_int(buf))
             try:
                 self.serial_handle.write(buf)
                 return True
@@ -224,7 +224,7 @@ class SerialIns(object):
 
             if len(self.receive_data) < 4:
                 break
-
+            print("read ori ", self.change_hex_to_int(self.receive_data))
             if self.receive_data[0] == 0x55 and self.receive_data[1] == 0x55:
                 # print("read ori ", self.change_hex_to_int(self.receive_data))
                 data_len = self.receive_data[2]

+ 1 - 9
python/mcu/capture/smart_shooter_class.py

@@ -460,14 +460,6 @@ class SmartShooter(metaclass=SingletonType):
                 "msg_type": self.msg_type,
                 "device_status": 2,
             }
-            msg_send = "相机未连接或软件未打开"
-            message = {
-                "code": 1,
-                "msg": msg_send,
-                "data": None,
-                "msg_type": self.msg_type,
-                "device_status": -1,
-            }
             await self.websocket_manager.send_personal_message(message, self.websocket)
             return True, "拍照成功"
         except zmq.Again:
@@ -565,7 +557,7 @@ class SmartShooter(metaclass=SingletonType):
                 # self.callback_listen(json_msg)
                 asyncio.run(self.callback_listen(json_msg))
             except zmq.Again:
-                print("接收超时,继续监听...")
+                # print("接收超时,继续监听...")
                 # logger.info("接收超时,继续监听...")
                 continue
             except Exception as e:

+ 105 - 0
python/mcu_test.py

@@ -0,0 +1,105 @@
+import time
+
+import serial
+import serial.tools.list_ports
+# from mcu.base_mode.base import *
+from mcu.SerialIns import SerialIns
+import asyncio
+
+class Main():
+    def __init__(self):
+        port_name = self.list_serial_ports()
+        if not port_name:
+            return
+        self.last_value = 0
+        self.serial_ins = SerialIns(port_name=port_name, baud=115200)
+    def encapsulation_data(self, data, len_data, data_magnification=1):
+        # data_magnification 数据放大倍数,或缩小倍数,默认为1
+        data = int(data * data_magnification)
+        if len_data == 1:
+            return [0xFF & data]
+        elif len_data == 2:
+            return [0xFF & data >> 8, 0xFF & data]
+        elif len_data == 4:
+            return [0xFF & data >> 24, 0xFF & data >> 16, 0xFF & data >> 8, 0xFF & data]
+    def list_serial_ports(self):
+        # 获取所有可用串口列表
+        ports = serial.tools.list_ports.comports()
+        # 遍历所有端口并打印信息
+        for port in ports:
+            print(f"设备: {port.device}")
+            print(f"名称: {port.name}")
+            print(f"描述: {port.description}")
+            print(f"硬件ID: {port.hwid}")
+            print(f"制造商: {port.manufacturer}")
+            print(f"产品ID: {port.product}")
+            print(f"序列号: {port.serial_number}")
+            print("-" * 40)
+            if "CH340" in port.description:
+                return port.name
+        return False
+
+    def run(self):
+        # loop = asyncio.get_event_loop()
+        loop = asyncio.get_event_loop()
+        listen_task = loop.run_in_executor(None, self.print_all())
+        asyncio.gather(listen_task)
+        n = 0
+        value = 0
+        while True:
+            value += 1
+            time.sleep(0.05)
+            n += 1
+            send_data = []
+            send_data.extend([0x01, 0x07, 0x01, 0x00, value, 0x05, 0x78, 0x01, 0x90, 0x00, 0x64, 0x00, 0x01, 0x00])
+            send_data.extend(self.encapsulation_data(data=n, len_data=4))
+            # send_data = [0x5a, 0x01]
+            if value == 200:
+                value = 0
+            self.serial_ins.write_cmd(send_data)
+            _send_data = self.serial_ins.change_hex_to_int(send_data)
+            _send_data = _send_data.strip()
+            # print("s buf:  {}".format(_send_data))
+    def get_data_from_receive_data(
+        self, receive_data, start, len_data, data_magnification=1
+    ):
+        # data_magnification 数据放大倍数,或缩小倍数,默认为1
+        try:
+            if len_data == 1:
+                data = receive_data[start]
+                return data * data_magnification
+            elif len_data == 2:
+                data = receive_data[start] << 8 | receive_data[start + 1]
+                return data * data_magnification
+            elif len_data == 4:
+                data = (
+                    receive_data[start] << 24
+                    | receive_data[start + 1] << 16
+                    | receive_data[start + 2] << 8
+                    | receive_data[start + 3]
+                )
+                return data * data_magnification
+            return None
+        except:
+            return None
+    def print_all(self):
+        while 1:
+            time.sleep(0.01)
+            r_data = self.serial_ins.read_cmd()
+            if r_data:
+                r_data = r_data[1:]
+                _r_data = self.serial_ins.change_hex_to_int(r_data)
+                _r_data = _r_data.strip()
+                print("g buf:{}".format(_r_data))
+
+                value = self.get_data_from_receive_data(receive_data=r_data, start=3, len_data=2, data_magnification=1)
+                print(value)
+                if value == 1 or value == self.last_value + 1:
+                    self.last_value = value
+                else:
+                    raise "数据接收有中断"
+
+
+if __name__ == '__main__':
+    main =  Main()
+    main.run()

+ 1 - 0
python/middleware.py

@@ -56,6 +56,7 @@ class UnicornException(Exception):
     def __init__(self, msg: str, code: int = -1):
         self.msg = msg
         self.code = code
+        self.message = msg
 
 
 @app.exception_handler(UnicornException)

+ 8 - 6
python/model/photo_record.py

@@ -1,6 +1,6 @@
 from sqlmodel import SQLModel, Field
 from datetime import datetime
-from typing import Optional
+from typing import Optional, Union
 import pytz
 
 TIME_ZONE = pytz.timezone("Asia/Shanghai")
@@ -12,9 +12,11 @@ class PhotoRecord(SQLModel, table=True):
     image_path: Optional[str] = Field(default=None)
     image_index: Optional[int] = Field(default=None)
     image_deal_mode: Optional[int] = Field(default=None)
-    photo_create_time: Optional[datetime] = Field(default=None)
-    update_time: Optional[datetime] = Field(
-        default_factory=lambda: datetime.now(TIME_ZONE)
+    photo_create_time: Optional[str] = Field(default=None)
+    update_time: Optional[str] = Field(
+        default_factory=lambda: datetime.now(TIME_ZONE).strftime("%Y-%m-%d %H:%M:%S")
     )
-    create_time: Optional[datetime] = Field(default_factory=lambda: datetime.now(TIME_ZONE))
-    delete_time: Optional[datetime] = Field(default=None)
+    create_time: Optional[str] = Field(
+        default_factory=lambda: datetime.now(TIME_ZONE).strftime("%Y-%m-%d %H:%M:%S")
+    )
+    delete_time: Optional[str] = Field(default=None)

+ 28 - 2
python/models.py

@@ -1,6 +1,6 @@
 from middleware import *
 import datetime
-
+from typing import Any
 
 class HlmForwardRequest(BaseModel):
     method: str = Field(default="GET", description="请求方法")
@@ -77,7 +77,8 @@ class TemplateItem(BaseModel):
     """模板项"""
 
     template_id: str = Field(description="模板名称")
-    template_local_classes: str = Field(description="模板名称")
+    template_local_classes: Any = Field(description="模板名称")
+    template_type : Optional[int] = Field(default=0, description="模板类型;0系统模板;1自定义模板")
 
 
 class MaineImageTest(BaseModel):
@@ -145,4 +146,29 @@ class SyncLocalConfigs(BaseModel):
     """同步系统配置"""
 
     token: str = Field(default=None, description="用户token")
+    env: str = Field(default="dev", description="当前环境")
+    
+
+class GenerateImageJson(BaseModel):
+    """货号json数据生成"""
+
+    goods_art_no: str = Field(default=None, description="货号")
+    
+    
+class PhotoRecordRemoveBackground(BaseModel):
+    """获取可执行程序命令列表"""
+
+    goods_art_nos: list[str] = Field(default=None, description="货号数组")
+    token: str = Field(default=None, description="用户token")
+    
+    
+class SyncPhotoRecord(BaseModel):
+    """同步图片记录"""
+
+    token: str = Field(default=None, description="用户token")
+    env: str = Field(default="dev", description="当前环境")
+    
     
+class RenameShadow(BaseModel):
+    """重命名阴影文件"""
+    goods_art_nos: list[str] = Field(default=None, description="货号数组")

+ 30 - 6
python/service/auto_deal_pics/base_deal.py

@@ -168,20 +168,44 @@ class BaseDealImage(object):
             return {'code': 1, 'msg': '图片位置与顺序重复,请检查您的输入'}
 
         for val in imageOrderList:
-            if val not in [
-                "正面",
+            image_orders = [
+                "俯视",
                 "侧视",
-                "背面",
-                "背侧",
+                "后跟",
+                "鞋底",
+                "内里",
                 "组合",
                 "组合2",
                 "组合3",
                 "组合4",
                 "组合5",
-            ]:
+                "组合6",
+                "组合7",
+                "组合8",
+                "组合9",
+                "组合10",
+                "组合11",
+                "组合12",
+                "组合13",
+                "组合14",
+                "组合15",
+                "组合16",
+                "组合17",
+                "组合18",
+                "组合19",
+                "组合20",
+                "组合21",
+                "组合22",
+                "组合23",
+                "组合24",
+                "组合25",
+                "组合26",
+            ]
+            if val not in image_orders:
+                image_orders_str = ','.join(map(str, image_orders))
                 return {
                     "code": 1,
-                    "msg": "可选项为:正面,侧视,背面,背侧,组合,组合2,组合3,组合4,组合5",
+                    "msg": f"可选项为:{image_orders_str}",
                 }
 
         if resize_image_view not in imageOrderList:

+ 32 - 5
python/service/base_deal.py

@@ -90,7 +90,7 @@ class BaseDealImage(object):
                 if windows.state != 1:
                     break
             folder_name = goods_art_no_folder_data["folder_name"]
-            callback_func("开始处理文件夹==========  {} ".format(folder_name))
+            callback_func("开始处理文件夹==========  {} ".format(all_goods_art_no_folder_data))
             if settings.IS_TEST:
                 flag = self.shoes_run_one_folder_to_deal(
                     goods_art_no_folder_data=goods_art_no_folder_data,
@@ -238,12 +238,13 @@ class BaseDealImage(object):
         imageOrderList = (
             image_order.replace(",", ",").replace(" ", "").replace("图", "").split(",")
         )
+        print("imageOrderList",imageOrderList)
         if len(set(imageOrderList)) != len(imageOrderList):
             return {"code": 1, "msg": "图片位置与顺序重复,请检查您的输入"}
 
         for val in imageOrderList:
-            if val not in [
-                "正面",
+            image_orders = [
+                "俯视",
                 "侧视",
                 "背面",
                 "背侧",
@@ -252,10 +253,34 @@ class BaseDealImage(object):
                 "组合3",
                 "组合4",
                 "组合5",
-            ]:
+                "组合6",
+                "组合7",
+                "组合8",
+                "组合9",
+                "组合10",
+                "组合11",
+                "组合12",
+                "组合13",
+                "组合14",
+                "组合15",
+                "组合16",
+                "组合17",
+                "组合18",
+                "组合19",
+                "组合20",
+                "组合21",
+                "组合22",
+                "组合23",
+                "组合24",
+                "组合25",
+                "组合26",
+            ]
+            if val not in image_orders:
+                print("val",val)
+                image_orders_str = ','.join(map(str, image_orders))
                 return {
                     "code": 1,
-                    "msg": "可选项为:正面,侧视,背面,背侧,组合,组合2,组合3,组合4,组合5",
+                    "msg": f"可选项为:{image_orders_str}",
                 }
 
         # if resize_image_view not in imageOrderList:
@@ -286,6 +311,8 @@ class BaseDealImage(object):
         self.check_path("{}/800x800".format(folder_path))
         self.crate_all_folders(folder_path)
         print("all_original_images====>", all_original_images)
+        if len(all_original_images) == 1:
+            time.sleep(1.5)
         if not all_original_images:
             return None
             # _ = ["俯视", "侧视", "后跟", "鞋底", "内里"]

+ 391 - 0
python/service/customer_template_service.py

@@ -0,0 +1,391 @@
+from email.policy import default
+from settings import *
+from middleware import UnicornException
+import copy
+import requests
+from PIL import Image
+from io import BytesIO
+import base64,shutil
+from logger import logger
+from natsort import ns, natsorted
+from service.base import get_images, check_path, get_image_mask
+from databases import DeviceConfig, SysConfigs, SqlQuery, CRUD, select, DeviceConfigTabs
+from model import PhotoRecord
+from sqlalchemy import and_, asc, desc
+'''前端生图接口'''
+# 
+generate_templace = "/generate"
+class CustomerTemplateService:
+    def __init__(self):
+        pass
+    
+    def parse_template_json(self,template_json):
+        """
+        解析 template_json 数据(如果它是 URL)
+
+        参数:
+        - template_json: str,模板数据(可能是 URL 或 JSON 字符串)
+
+        返回:
+        - dict,解析后的 JSON 数据
+        """
+        try:
+            # 检查是否为 URL
+            if isinstance(template_json, str) and (template_json.startswith("http://") or template_json.startswith("https://")):
+                # 发送 GET 请求获取数据
+                response = requests.get(template_json)
+                response.raise_for_status()  # 检查请求是否成功
+                # 解析 JSON 数据
+                parsed_data = response.json()
+                return parsed_data
+            else:
+                # 如果不是 URL,直接解析为 JSON
+                return json.loads(template_json)
+        except requests.exceptions.RequestException as e:
+            print(f"网络请求失败: {e}")
+            return None
+        except json.JSONDecodeError as e:
+            print(f"JSON 解析失败: {e}")
+            return None
+    
+    def generateTemplate(self,config_data,template_json,template_name,save_path):
+        '''
+        参数:
+        config_data: 配置数据
+        template_json: 模板数据
+        template_name: 模板名称
+        save_path: 保存路径
+        '''
+        print("开始生成模板")
+        template_json_data = self.parse_template_json(template_json)
+        # print("config_data",config_data)
+        handler_config_data,model_image,scene_image = self.__handler_config_data(config_data,save_path)
+        print("handler_config_data",handler_config_data)
+        self.goods_no_value = handler_config_data
+        headers = {"Content-Type": "application/json"}
+        json_data = {"goodsList":[{self.goods_no:handler_config_data}],"canvasList":template_json_data}
+        json_data = json.dumps(json_data,ensure_ascii=False)
+        # print("json_data",json_data)
+        template_result = requests.post(CUSTOMER_TEMPLATE_URL+generate_templace,data=json_data,headers=headers)
+        resultJson = template_result.json()
+        code = resultJson.get("code")
+        msg = resultJson.get("msg")
+        images = resultJson.get("images",[])
+        if code != 0:
+            raise UnicornException(f"详情页生成失败,请检查模板数据是否正确:{msg}")
+        concat_images_array = []
+        for image in images:
+            canvasIndex = image.get("canvasIndex")
+            dataUrl = image.get("dataUrl")
+            save_name = f"{save_path}/切片图-{template_name}/{self.goods_no}({int(canvasIndex)+1}).png"
+            match dataUrl:
+                case "model":
+                    # 复制模特图进行拼接
+                    if model_image:
+                        model_copy_res = self.copyImage(model_image,save_name)
+                        if model_copy_res:
+                            model_image_pil = Image.open(model_image)
+                            concat_images_array.append(model_image_pil)
+                case "scene":
+                    # 复制场景图进行拼接
+                    if scene_image:
+                        scene_copy_res = self.copyImage(scene_image,save_name)
+                        if scene_copy_res:
+                            scene_image_pil = Image.open(scene_image)
+                            concat_images_array.append(scene_image_pil)
+                case _:
+                    pillowImage = self.save_base64_image(dataUrl,save_name)
+                    concat_images_array.append(pillowImage)
+        long_image = self.concat_images_vertically(concat_images_array)
+        if long_image:
+            save_name = f"{save_path}/详情页-{template_name}.jpg"
+            long_image.save(save_name,format="JPEG")
+            self.move_other_pic(move_main_pic=True,save_path=save_path)
+        print("模板生成成功")
+        try:
+            # 删除 800x800 目录及其内容
+            directory_to_remove = os.path.join(save_path, "800x800")
+            if os.path.exists(directory_to_remove):
+                shutil.rmtree(directory_to_remove)
+                print(f"目录 {directory_to_remove} 已成功删除")
+        except Exception as e:
+            print(f"删除目录失败: {e}")
+    def create_folder(self, path):
+        # 创建目录
+        if  path and not os.path.exists(path):
+            print(f"创建目录   详情页=================>>>>:{path}")
+            os.makedirs(path)
+    def move_other_pic(self, move_main_pic=True,save_path=""):
+        sorted_list_800 = []
+        for goods_art_no_dict in self.goods_no_value["货号资料"]:
+            if "800x800" not in goods_art_no_dict:
+                continue
+            if not goods_art_no_dict["800x800"]:
+                continue
+            goods_art_no = ""
+            if "编号" in goods_art_no_dict:
+                if goods_art_no_dict["编号"]:
+                    goods_art_no = goods_art_no_dict["编号"]
+            if not goods_art_no:
+                goods_art_no = goods_art_no_dict["货号"]
+            sorted_list_800 = natsorted(goods_art_no_dict["800x800"], key=lambda x: x.split("(")[1].split(")")[0])
+            self.create_folder(save_path)
+            # 放入一张主图
+            old_pic_path_1 = sorted_list_800[0]
+            shutil.copy(
+                old_pic_path_1,
+                "{}/颜色图{}{}".format(
+                    save_path, goods_art_no, os.path.splitext(old_pic_path_1)[1]
+                ),
+            )
+
+            # 把其他主图放入作为款号图=====================
+            if move_main_pic:
+                for idx,pic_path in enumerate(sorted_list_800):
+                    index = idx + 1
+                    try:
+                        split_size = pic_path.split("_")[1].split(".")[0]
+                    except:
+                        split_size = ""
+                    suffix_name = "_"+split_size if split_size else ""
+                    print("pic_path=========>",split_size)
+                    e = os.path.splitext(pic_path)[1]
+                    shutil.copy(
+                        pic_path,
+                        "{out_put_dir}/主图{goods_no}({goods_no_main_pic_number}){suffix_name}{e}".format(
+                            out_put_dir=save_path,
+                            goods_no=goods_art_no,
+                            goods_no_main_pic_number=str(
+                                index
+                            ),
+                            e=e,
+                            suffix_name=suffix_name
+                        ),
+                    )
+    def concat_images_vertically(self,image_array, custom_width=None):
+        """
+        按照顺序将图片数组拼接成长图,并统一图片宽度
+
+        参数:
+        - image_array: list,存放 Pillow 图片对象的数组
+        - custom_width: int,可选参数,指定统一的图片宽度(默认为第一张图的宽度)
+
+        返回:
+        - concatenated_image: PIL.Image,拼接后的长图对象
+        """
+        if not image_array:
+            return None
+
+        # 1. 确定统一宽度
+        base_width = custom_width or image_array[0].width
+
+        # 2. 计算总高度和调整图片尺寸
+        total_height = 0
+        resized_images = []
+        for img in image_array:
+            # 调整图片宽度并保持宽高比
+            width_ratio = base_width / img.width
+            new_height = int(img.height * width_ratio)
+            resized_img = img.resize((base_width, new_height), Image.Resampling.LANCZOS)
+            resized_images.append(resized_img)
+            total_height += new_height
+
+        # 3. 创建空白画布
+        concatenated_image = Image.new("RGB", (base_width, total_height))
+
+        # 4. 按顺序拼接图片
+        y_offset = 0
+        for resized_img in resized_images:
+            concatenated_image.paste(resized_img, (0, y_offset))
+            y_offset += resized_img.height
+
+        return concatenated_image
+    def get_config_name(self,good_art_no):
+        session = SqlQuery()
+        query = (
+            select(PhotoRecord, DeviceConfig.action_name)
+            .outerjoin(DeviceConfig, PhotoRecord.action_id == DeviceConfig.id)
+            .where(PhotoRecord.goods_art_no == good_art_no)
+            .where(PhotoRecord.delete_time == None)
+            .order_by(PhotoRecord.goods_art_no, asc("id"))  # 按货号分组并按ID倒序
+        )
+        all_items = session.exec(query).mappings().all()
+        if len(all_items) == 0:  # 如果没有记录则返回
+            # raise UnicornException("暂无可用货号")
+            return None
+        goods_art_rename_list = []
+        for index,item in enumerate(all_items):
+            image_orders = [
+                "俯视",
+                "侧视",
+                "后跟",
+                "鞋底",
+                "内里",
+                "组合",
+                "组合2",
+                "组合3",
+                "组合4",
+                "组合5",
+                "组合6",
+                "组合7",
+                "组合8",
+                "组合9",
+                "组合10",
+                "组合11",
+                "组合12",
+                "组合13",
+                "组合14",
+                "组合15",
+                "组合16",
+                "组合17",
+                "组合18",
+                "组合19",
+                "组合20",
+                "组合21",
+                "组合22",
+                "组合23",
+                "组合24",
+                "组合25",
+                "组合26",
+            ]
+            if item["action_name"] == None:
+                continue
+            data = {"goods_art_no": item.PhotoRecord.goods_art_no,"old_name":image_orders[index], "action_name": item["action_name"],"image_index":item.PhotoRecord.image_index}
+            goods_art_rename_list.append(data)
+        session.close()
+        return goods_art_rename_list
+    def __handler_config_data(self,config_data,scp_path):
+        '''
+        处理配置数据,返回一个新的数据对象
+        '''
+        directory = os.path.dirname(scp_path)
+        self.create_folder(directory)
+        # 深拷贝原始数据,确保不改变原数据对象
+        new_config_data = copy.deepcopy(config_data)
+        print("传入的config数据",new_config_data)
+        model_image = None
+        scene_image = None
+        self.goods_no = new_config_data.get("款号")
+        # 如果输入是字典,则将其转换为目标结构
+        # 提取需要添加的数据,排除特定字段
+        additional_data = {k: v for k, v in new_config_data.items() if k not in ["款号", "货号资料"]}
+        # 遍历货号资料,将额外数据添加到每个货号对象中
+        for product in new_config_data.get("货号资料", []):
+            product.update(additional_data)
+            # 处理 pics 字段中的 xx-抠图 转换为 Base64 并新增字段
+            pics = product.get("pics", {})
+            goods_art_no = product.get("货号",None)
+            goods_art_lens = len(new_config_data.get("货号资料", []))
+            goods_art_no_rename_list = self.get_config_name(goods_art_no)
+            concat_shuffix = "" if goods_art_lens == 1 else f"_{goods_art_no}"
+            if not model_image:
+                model_image_path = product.get("模特图", None)
+                if model_image_path:
+                    model_image = model_image_path
+                    self.copyImage(model_image_path, f"{scp_path}/模特图{concat_shuffix}.jpg")
+            if not scene_image:
+                scene_image_path = product.get("场景图", None)
+                if scene_image_path:
+                    scene_image = scene_image_path
+                    self.copyImage(scene_image_path, f"{scp_path}/场景图{concat_shuffix}.jpg")
+            new_pics = {}
+            index = 0
+            for pic_key, pic_path in pics.items():
+                if "-抠图" in pic_key:
+                    # 读取图片并转换为 Base64
+                    try:
+                        base64_data = self.crop_image_and_convert_to_base64(pic_path)
+                        # 新增字段(去除 -抠图)
+                        new_key = pic_key.replace("-抠图", "")
+                        try:
+                            new_key = goods_art_no_rename_list[index].get("action_name")
+                        except:
+                            print("没有找到对应的action_name",index)
+                        # print("goods_art_no_rename_list",goods_art_no_rename_list)
+                        new_pics[new_key] = base64_data
+                    except Exception as e:
+                        print(f"读取图片失败: {pic_path}, 错误: {e}")
+                    index += 1
+                else:
+                    # 非 -抠图 字段保持不变
+                    # new_pics[pic_key] = pic_path
+                    pass
+                # 更新 pics 字段
+                product["pics"] = new_pics
+            
+                # 构建目标结构
+            #     result.append({key: item})
+            # return result
+        return new_config_data,model_image,scene_image
+    def save_base64_image(self,base64_data, output_path):
+        """
+        将 Base64 编码的图像保存到本地文件
+
+        参数:
+        - base64_data: str,Base64 编码的图像数据(不包含前缀如 "data:image/png;base64,")
+        - output_path: str,保存图像的本地路径
+        """
+        if "data:image/jpeg;base64," in base64_data:
+            base64_data = base64_data.split(",")[1]
+        try:
+            # 1. 解码 Base64 数据
+            image_data = base64.b64decode(base64_data)
+            
+            # 2. 加载图像数据
+            image = Image.open(BytesIO(image_data))
+            # 3. 检查路径是否存在,如果不存在则创建
+            directory = os.path.dirname(output_path)
+            self.create_folder(directory)
+            # 4. 保存图像到本地
+            image.save(output_path)
+            print(f"图像已成功保存到 {output_path}")
+            return image
+        except Exception as e:
+            print(f"保存图像失败: {e}")
+            print(f"Base64 数据前 100 字符: {base64_data[:100]}")
+            return None
+            
+    def crop_image_and_convert_to_base64(self,pic_path):
+        """
+        使用 Pillow 裁剪图片并生成带有前缀的 Base64 图片
+
+        参数:
+        - pic_path: str,图片文件路径
+
+        返回:
+        - base64_data_with_prefix: str,带有前缀的 Base64 编码图片数据
+        """
+        try:
+            # 1. 加载图片
+            with Image.open(pic_path) as image:
+                # 2. 获取图片的非透明区域边界框 (bounding box)
+                bbox = image.getbbox()
+                if not bbox:
+                    raise ValueError("图片可能是完全透明的,无法获取边界框")
+                
+                # 3. 裁剪图片
+                cropped_image = image.crop(bbox)
+                
+                # 4. 将裁剪后的图片转换为 Base64
+                buffered = BytesIO()
+                cropped_image.save(buffered, format="PNG")  # 保存为 PNG 格式
+                base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
+                
+                # 5. 添加 Base64 前缀
+                base64_data_with_prefix = f"data:image/png;base64,{base64_data}"
+                return base64_data_with_prefix
+
+        except Exception as e:
+            print(f"处理图片失败: {pic_path}, 错误: {e}")
+            return None
+    def copyImage(self,src_path,limit_path):
+        try:
+            directory = os.path.dirname(limit_path)
+            self.create_folder(directory)
+            print("copyImage 模型图/场景图开始复制",src_path)
+            print("copyImage 模型图/场景图开始复制",limit_path)
+            shutil.copy(src_path, limit_path)
+            return True
+        except Exception as e:
+            logger.info(f"copyImage 复制模特图/场景图出错:{str(e)}")
+            return False

+ 11 - 11
python/service/data.py

@@ -169,10 +169,10 @@ class DataModeGenerateDetail(DataBaseModel):
     def get_basic_goods_art_data_form_excel(self, folder_name_list, excel_path, keys):
 
         # =====创建虚拟表格并进行连表处理
-        need_df = pd.DataFrame(columns=["文件夹名称"])
+        need_df = pd.DataFrame(columns=["商品货号"])
         for folder_name in folder_name_list:
             new_row = {
-                "文件夹名称": str(folder_name),
+                "商品货号": str(folder_name),
             }
             need_df = need_df._append(new_row, ignore_index=True)
 
@@ -181,7 +181,7 @@ class DataModeGenerateDetail(DataBaseModel):
         # 打开表格并进行匹配
         _df = pd.read_excel(excel_path, sheet_name=0, header=0)
         # 去重数据
-        duplicates = _df.duplicated(subset=["文件夹名称"], keep="first")
+        duplicates = _df.duplicated(subset=["商品货号"], keep="first")
         _df = _df.loc[~duplicates]
         _df = _df.fillna(value="")
         _df = _df.astype(str)
@@ -189,7 +189,7 @@ class DataModeGenerateDetail(DataBaseModel):
         need_df = pd.merge(
             need_df,
             _df,
-            on=["文件夹名称"],
+            on=["商品货号"],
             how="left",
             indicator=False,
         )
@@ -206,11 +206,11 @@ class DataModeGenerateDetail(DataBaseModel):
         message = ""
         for index, row in need_df.iterrows():
             if settings.PROJECT == "红蜻蜓":
-                if row["商品货号"] and row["款号"] and row["编号"]:
-                    return_dict[row["文件夹名称"]] = {
+                if row["商品货号"] and row["款号"]:
+                    return_dict[row["商品货号"]] = {
                         "type": "goods_art_no",
-                        "name": row["文件夹名称"].upper(),
-                        "文件夹名称": row["文件夹名称"],
+                        "name": row["商品货号"].upper(),
+                        "商品货号": row["商品货号"],
                         "template_name": row["模板名称"],
                         "data": row.to_dict(),
                     }
@@ -218,10 +218,10 @@ class DataModeGenerateDetail(DataBaseModel):
                     message = "商品货号、款号、编号必须有值"
             else:
                 if row["商品货号"] and row["款号"]:
-                    return_dict[row["文件夹名称"]] = {
+                    return_dict[row["商品货号"]] = {
                         "type": "goods_art_no",
-                        "name": row["文件夹名称"].upper(),
-                        "文件夹名称": row["文件夹名称"],
+                        "name": row["商品货号"].upper(),
+                        "商品货号": row["商品货号"],
                         "template_name": row["模板名称"],
                         "data": row.to_dict(),
                     }

+ 1 - 1
python/service/deal_one_image.py

@@ -291,7 +291,7 @@ class DealOneImage(Base):
                     image_deal_info["抠图扩边后位置"][1],
                 ),
             )
-            _img_im.save(self.out_path)
+            _img_im.save(self.out_path,dpi=(350, 350))
 
             self.send_info(text="{} 抠图已完成".format(self.file_name), is_success=True)
             return self.file_path

+ 10 - 11
python/service/generate_goods_no_detail_pic/data.py

@@ -107,9 +107,9 @@ class DataModeGenerateDetail(DataBaseModel):
     def get_basic_goods_art_data_form_excel(self, folder_name_list, excel_path, keys):
 
         # =====创建虚拟表格并进行连表处理
-        need_df = pd.DataFrame(columns=["文件夹名称"])
+        need_df = pd.DataFrame(columns=["商品货号"])
         for folder_name in folder_name_list:
-            new_row = {"文件夹名称": str(folder_name),
+            new_row = {"商品货号": str(folder_name),
                        }
             need_df = need_df._append(new_row, ignore_index=True)
 
@@ -118,13 +118,13 @@ class DataModeGenerateDetail(DataBaseModel):
         # 打开表格并进行匹配
         _df = pd.read_excel(excel_path, sheet_name=0, header=0)
         # 去重数据
-        duplicates = _df.duplicated(subset=['文件夹名称'], keep="first")
+        duplicates = _df.duplicated(subset=['商品货号'], keep="first")
         _df = _df.loc[~duplicates]
         _df = _df.fillna(value='')
         _df = _df.astype(str)
 
         # 数据匹配关联,左关联
-        need_df = pd.merge(need_df, _df, on=["文件夹名称"], how="left", indicator=False, )
+        need_df = pd.merge(need_df, _df, on=["商品货号"], how="left", indicator=False, )
         # 补全字段
         header_list = need_df.columns.values.tolist()
         for key in keys:
@@ -138,11 +138,10 @@ class DataModeGenerateDetail(DataBaseModel):
         message = ""
         for index, row in need_df.iterrows():
             if settings.PROJECT == "红蜻蜓":
-                if row["商品货号"] and row["款号"] and row["编号"]:
-                    return_dict[row["文件夹名称"]] = {
+                if row["商品货号"] and row["款号"]:
+                    return_dict[row["商品货号"]] = {
                         "type": "goods_art_no",
-                        "name": row["文件夹名称"].upper(),
-                        "文件夹名称": row["文件夹名称"],
+                        "name": row["商品货号"].upper(),
                         "template_name": row["模板名称"],
                         "data": row.to_dict(),
                     }
@@ -150,10 +149,10 @@ class DataModeGenerateDetail(DataBaseModel):
                     message = "商品货号、款号、编号必须有值"
             else:
                 if row["商品货号"] and row["款号"]:
-                    return_dict[row["文件夹名称"]] = {
+                    return_dict[row["商品货号"]] = {
                         "type": "goods_art_no",
-                        "name": row["文件夹名称"].upper(),
-                        "文件夹名称": row["文件夹名称"],
+                        "name": row["商品货号"].upper(),
+                        "文件夹名称": row["商品货号"],
                         "template_name": row["模板名称"],
                         "data": row.to_dict(),
                     }

+ 6 - 8
python/service/generate_main_image/grenerate_main_image_test.py

@@ -457,7 +457,6 @@ class GeneratePic(object):
             opacity = (average_brightness - 30) / 160
             opacity = max(0.5, min(opacity, 1))
 
-            print("阴影透明度:{}%".format(int(opacity * 100)))
             blended_np = multiply(
                 backdrop_prepped, source_prepped, opacity=int(opacity * 100) / 100
             )
@@ -477,7 +476,7 @@ class GeneratePic(object):
                 out_image_1 = out_image_1.transpose(Image.FLIP_LEFT_RIGHT)
 
             self.saver.save_image(
-                image=out_image_1, file_path=out_process_path_1, save_mode="png"
+                image=out_image_1, file_path=out_process_path_1, quality=100,dpi=(350, 350), _format="PNG"
             )
             # save_image_by_thread(image=out_image_1, out_path=out_process_path_1)
             # out_image_1.save(out_process_path_1)
@@ -489,7 +488,7 @@ class GeneratePic(object):
                 out_image_2 = out_image_2.transpose(Image.FLIP_LEFT_RIGHT)
 
             self.saver.save_image(
-                image=out_image_2, file_path=out_process_path_2, save_mode="png"
+                image=out_image_2, file_path=out_process_path_2, quality=100,dpi=(350, 350), _format="PNG"
             )
             # save_image_by_thread(image=out_image_2, out_path=out_process_path_2, save_mode="png")
             # out_image_2.save(out_process_path_2)
@@ -595,8 +594,8 @@ class GeneratePic(object):
                     image=image_bg,
                     file_path=out_path,
                     save_mode="jpg",
-                    quality=None,
-                    dpi=None,
+                    quality=100,
+                    dpi=(350, 350),
                     _format="JPEG",
                 )
                 # save_image_by_thread(image_bg, out_path, save_mode="jpg", quality=None, dpi=None, _format="JPEG")
@@ -605,10 +604,9 @@ class GeneratePic(object):
                 self.saver.save_image(
                     image=image_bg,
                     file_path=out_path,
-                    save_mode="jpg",
                     quality=100,
-                    dpi=(300, 300),
-                    _format="JPEG",
+                    dpi=(350, 350),
+                    _format="PNG",
                 )
                 # save_image_by_thread(image_bg, out_path, save_mode="jpg", quality=100, dpi=(300, 300), _format="JPEG")
                 # image_bg.save(out_path, quality=100, dpi=(300, 300), format="JPEG")

+ 171 - 53
python/service/grenerate_main_image_test.py

@@ -12,6 +12,7 @@ from .multi_threaded_image_saving import ImageSaver
 from .get_mask_by_green import GetMask
 from middleware import UnicornException
 from logger import logger
+from custom_plugins.plugins_mode.pic_deal import PictureProcessing
 def time_it(func):
     @wraps(func)  # 使用wraps来保留原始函数的元数据信息
     def wrapper(*args, **kwargs):
@@ -32,7 +33,113 @@ class GeneratePic(object):
         self.is_test = is_test
         self.saver = ImageSaver()
         pass
+    @time_it
+    def get_mask_and_config_v3(self, im_jpg: Image, im_png: Image, curve_mask: bool,grenerate_main_pic_brightness:int):
+        """
+        步骤:
+        1、尺寸进行对应缩小
+        2、查找并设定鞋底阴影蒙版
+        3、自动色阶检查亮度
+        4、输出自动色阶参数、以及放大的尺寸蒙版
+        """
+        # ===================尺寸进行对应缩小(提升处理速度)
+        im_jpg = to_resize(im_jpg, width=600)
+        im_png = to_resize(im_png, width=600)
+
+
+        # =========================两个蒙版叠加,删除上半部分的图
+        # 获取透明图的左右点
+        result = get_extremes_from_transparent(im_png)
+        # 创建多边形mask(并进行左右偏移)
+        left_point = (result["leftmost"][0], result["leftmost"][1] - 50)
+        right_point = (result["rightmost"][0], result["rightmost"][1] - 50)
+        mask_other_2 = create_polygon_mask_from_points(img=im_png, left_point=left_point, right_point=right_point)
+
+        # 透明图转mask 将原图扩边一些,并填充白色
+        mask_other_1 = transparent_to_mask_pil(im_png, is_invert=False)
+        mask_other_1 = expand_or_shrink_mask(pil_image=mask_other_1, expansion_radius=40, blur_radius=0)
+        new_image_1 = Image.new("RGBA", im_png.size, (255, 255, 255, 0))
+        im_grey_jpg = im_jpg.convert("L").convert("RGB")
+        inverted_mask_other_1 = ImageChops.invert(mask_other_1)
+
+        # 两个mask 取交集
+        mask_other_2 = mask_other_2.convert("L")
+        # 返回的蒙版区域
+        return_mask = mask_other_2
+
+        new_mask = mask_intersection(inverted_mask_other_1, mask_other_2)
+        # new_mask.show()
+        # return_mask.show()
+        # TODO 待移除
+
+        # ====================生成新的图片
+        print("84  生成新的图片")
+        bg = Image.new(mode="RGB", size=im_png.size, color=(255, 255, 255))
+        bg.paste(im=im_jpg, mask=new_mask)  # 只粘贴有阴影的地方
+        # bg.show()
+
+        # ==================自动色阶处理======================
+        # 对上述拼接后的图片进行自动色阶处理
+        _im = cv2.cvtColor(np.asarray(bg), cv2.COLOR_RGB2BGR)
+        # 背景阴影
+        im_shadow = cv2.cvtColor(_im, cv2.COLOR_BGR2GRAY)
 
+        print("copy.copy(im_shadow)")
+        _im_shadow = copy.copy(im_shadow)
+        Midtones = 0.7
+        Highlight = 235
+        k = copy.copy(settings.COLOR_GRADATION_CYCLES)
+        print("开始循环识别")
+        xunhuan = 0
+        while k:
+            xunhuan += 1
+            k -= 1
+            Midtones += 0.035
+            if Midtones > 1.7:
+                Midtones = 1.7
+            Highlight -= 3
+
+            _im_shadow = levels_adjust(img=im_shadow,
+                                       Shadow=0,
+                                       Midtones=Midtones,
+                                       Highlight=Highlight,
+                                       OutShadow=0,
+                                       OutHighlight=255, Dim=3)
+
+            brightness_value = brightness_check(img_gray=_im_shadow, mask=new_mask)
+
+            print("循环识别:{},Midtones:{},Highlight:{},brightness_value:{}".format(xunhuan,
+                                                                                Midtones,
+                                                                                Highlight,
+                                                                                brightness_value))
+                                                                                    
+
+            if brightness_value >= grenerate_main_pic_brightness:
+                # //GRENERATE_MAIN_PIC_BRIGHTNESS 亮度校验
+                break
+
+        im_shadow = cv2_to_pil(_im_shadow)
+        # if self.is_test:
+        #     im_shadow.show()
+
+        # ========================================================
+        # 计算阴影的亮度,用于确保阴影不要太黑
+
+        # 1、图片预处理,只保留阴影
+        only_shadow_img = im_shadow.copy()
+        only_shadow_img.paste(Image.new(mode="RGBA", size=only_shadow_img.size, color=(255, 255, 255, 255)),
+                              mask=im_png)
+        # only_shadow_img.show()
+        average_brightness = calculated_shadow_brightness(only_shadow_img)
+        print("average_brightness:", average_brightness)
+
+        config = {
+            "Midtones": Midtones,
+            "Highlight": Highlight,
+            "average_brightness": average_brightness,
+        }
+
+        return return_mask, config
     @time_it
     def get_mask_and_config(self, im_jpg: Image, im_png: Image, curve_mask: bool):
         """
@@ -470,6 +577,10 @@ class GeneratePic(object):
         padding_800image = settings.getSysConfigs(
             "basic_configs", "padding_800image", 100
         )
+        color_800image = settings.getSysConfigs(
+            "basic_configs", "color_800image", "#FFFFFF"
+        )
+        rgb_color = settings.hex_to_rgb(color_800image)
         # ==========先进行剪切原图
         _s = time.time()
         with Image.open(image_path) as orign_im:
@@ -486,9 +597,17 @@ class GeneratePic(object):
 
         # ================自动色阶处理
         _s = time.time()
-        shadow_mask, config = self.get_mask_and_config(
-            im_jpg=im_shadow, im_png=cut_image, curve_mask=curve_mask
-        )
+        image_mask_config = settings.getSysConfigs("basic_configs", "image_mask_config", {"mode":0,"opacity":0.5,"grenerate_main_pic_brightness":254})
+        print("阴影图处理参数===>>>",image_mask_config)
+        image_mask_mode = image_mask_config.get("mode",0)
+        image_mask_opacity = float(image_mask_config.get("opacity",0.5))
+        image_mask_grenerate_main_pic_brightness = int(image_mask_config.get("grenerate_main_pic_brightness",254))
+        if image_mask_mode ==0:
+            shadow_mask, config = self.get_mask_and_config(
+                im_jpg=im_shadow, im_png=cut_image, curve_mask=curve_mask
+            )
+        else:
+            shadow_mask, config = self.get_mask_and_config_v3(im_jpg=im_shadow, im_png=cut_image, curve_mask=curve_mask,grenerate_main_pic_brightness=image_mask_grenerate_main_pic_brightness)
         print("242  need_time_2:{}".format(time.time() - _s))
 
         shadow_mask = shadow_mask.resize(im_shadow.size)
@@ -516,25 +635,37 @@ class GeneratePic(object):
 
         # ================处理阴影的亮度==================
         average_brightness = config["average_brightness"]
-        if config["average_brightness"] < 180:
-            # 调整阴影亮度
+        if image_mask_mode ==0:
+            if config["average_brightness"] < 180:
+                # 调整阴影亮度
+                backdrop_prepped = np.asfarray(
+                    Image.new(mode="RGBA", size=im_shadow.size, color=(255, 255, 255, 255))
+                )
+                im_shadow = im_shadow.convert("RGBA")
+                source_prepped = np.asfarray(im_shadow)
+                # im_shadow.show()
+
+                opacity = (average_brightness - 30) / 160
+                opacity = max(0.5, min(opacity, 1))
+
+                print("阴影透明度:{}%".format(int(opacity * 100)))
+                blended_np = multiply(
+                    backdrop_prepped, source_prepped, opacity=int(opacity * 100) / 100
+                )
+                im_shadow = Image.fromarray(np.uint8(blended_np)).convert("RGB")
+            # im_shadow.show()
+        else:
             backdrop_prepped = np.asfarray(
-                Image.new(mode="RGBA", size=im_shadow.size, color=(255, 255, 255, 255))
-            )
+                    Image.new(mode="RGBA", size=im_shadow.size, color=(255, 255, 255, 255))
+                )
             im_shadow = im_shadow.convert("RGBA")
             source_prepped = np.asfarray(im_shadow)
-            # im_shadow.show()
-
-            opacity = (average_brightness - 30) / 160
-            opacity = max(0.5, min(opacity, 1))
-
-            print("阴影透明度:{}%".format(int(opacity * 100)))
+            opacity_params = int(image_mask_opacity * 100)
+            print("阴影透明度:{}%".format(opacity_params))
             blended_np = multiply(
-                backdrop_prepped, source_prepped, opacity=int(opacity * 100) / 100
+                backdrop_prepped, source_prepped, opacity=opacity_params / 100
             )
             im_shadow = Image.fromarray(np.uint8(blended_np)).convert("RGB")
-            # im_shadow.show()
-
         # 把原图粘贴回去,避免色差
         im_shadow.paste(cut_image, (0, 0), mask=cut_image)
         # _new_im_shadow.show()
@@ -548,7 +679,7 @@ class GeneratePic(object):
                 out_image_1 = out_image_1.transpose(Image.FLIP_LEFT_RIGHT)
 
             self.saver.save_image(
-                image=out_image_1, file_path=out_process_path_1, save_mode="png"
+                image=out_image_1, file_path=out_process_path_1, quality=100,dpi=(350, 350), _format="PNG"
             )
             # save_image_by_thread(image=out_image_1, out_path=out_process_path_1)
             # out_image_1.save(out_process_path_1)
@@ -560,7 +691,7 @@ class GeneratePic(object):
                 out_image_2 = out_image_2.transpose(Image.FLIP_LEFT_RIGHT)
 
             self.saver.save_image(
-                image=out_image_2, file_path=out_process_path_2, save_mode="png"
+                image=out_image_2, file_path=out_process_path_2, quality=100,dpi=(350, 350), _format="PNG"
             )
             # save_image_by_thread(image=out_image_2, out_path=out_process_path_2, save_mode="png")
             # out_image_2.save(out_process_path_2)
@@ -620,42 +751,33 @@ class GeneratePic(object):
                         im_shadow = to_resize(_im=im_shadow, high=1400)
                         cut_image = to_resize(_im=cut_image, high=1400)
 
-                    # if im_shadow.height <= im_shadow.width * 1.2:
-                    #     im_shadow = to_resize(_im=im_shadow, width=650)
-                    #     cut_image = to_resize(_im=cut_image, width=650)
-                    # else:
-                    #     im_shadow = to_resize(_im=im_shadow, high=1400)
-                    #     cut_image = to_resize(_im=cut_image, high=1400)
-
 
         # 创建底层背景
-        image_bg = Image.new("RGB", bg_size, (255, 255, 255))
-        image_bg = self.paste_img(image=image_bg, top_img=im_shadow, base="cc", value=(_offset_x * -1, _offset_y * -1))
-        image_bg = self.paste_img(image=image_bg, top_img=cut_image, base="cc", value=(_offset_x * -1, _offset_y * -1))
+        # 用户可设置的颜色值参数
+        # image_bg = Image.new("RGB", bg_size, rgb_color)
+        # image_bg = self.paste_img(image=image_bg, top_img=im_shadow, base="cc", value=(_offset_x * -1, _offset_y * -1))
+        # image_bg = self.paste_img(image=image_bg, top_img=cut_image, base="cc", value=(_offset_x * -1, _offset_y * -1))
+        image_bg = PictureProcessing("RGB", bg_size, rgb_color)
+        image_bg = image_bg.to_overlay_pic_advance(mode="pixel",
+                                                    top_img=PictureProcessing(im=im_shadow),
+                                                    base="cc",
+                                                    value=(_offset_x * -1, _offset_y * -1),
+                                                    top_png_img=PictureProcessing(im=cut_image),)
+        image_bg = image_bg.im
         image_bg_x, image_bg_y = image_bg.size
         image_x, image_y = im_shadow.size
 
         _x = int((image_bg_x - image_x) / 2)
         _y = int((image_bg_y - image_y) / 2)
 
-        image_bg.paste(im_shadow, (_x, _y))
-        image_bg.paste(cut_image, (_x, _y), cut_image)  # 再叠加原图避免色差
+        # image_bg.paste(im_shadow, (_x, _y))
+        # image_bg.paste(cut_image, (_x, _y), cut_image)  # 再叠加原图避免色差
 
         if "小苏" in settings.Company:
             # 所有主图加logo
             is_logo = True
 
         if is_logo:
-            # logo_path = ""
-            # if settings.PROJECT == "红蜻蜓":
-            #     logo_path = r"resources\LOGO\HQT\logo.png"
-            # elif settings.PROJECT == "惠利玛":
-            #     if "小苏" in settings.Company:
-            #         logo_path = r"resources\LOGO\xiaosushuoxie\logo.png"
-            #     elif "惠利玛" in settings.Company:
-            #         logo_path = r"resources\LOGO\HLM\logo.png"
-            #     else:
-            #         pass
             if not logo_path:
                 logo_im = Image.new("RGBA", (1600, 1600), (0, 0, 0, 0))
             else:
@@ -676,7 +798,6 @@ class GeneratePic(object):
             if settings.getSysConfigs("basic_configs", "image_sharpening", "1") == ""
             else settings.getSysConfigs("basic_configs", "image_sharpening", "1")
         )
-        # image_bg = image_bg.resize((out_pic_size, out_pic_size), Image.BICUBIC)
         if out_pci_factor > 1.0:
             print("图片锐化处理")
             image_bg = sharpen_image(image_bg, factor=out_pci_factor)
@@ -696,29 +817,26 @@ class GeneratePic(object):
             image_size_int = int(imageSize)
             image_size_str = str(imageSize)
             new_file_path = f"{file_without_suffix}_{image_size_str}.{suffix}"
-            if image_size_int < 3000:
-                image_bg = image_bg.resize(
+            image_bg = image_bg.resize(
                     (image_size_int, image_size_int), resample=settings.RESIZE_IMAGE_MODE
                 )
+            if image_size_int < 3000:
                 if out_pci_mode == ".jpg":
                     self.saver.save_image(
                         image=image_bg,
                         file_path=new_file_path,
                         save_mode="jpg",
-                        quality=None,
-                        dpi=None,
+                        quality=100,
+                        dpi=(350, 350),
                         _format="JPEG",
                     )
-                    # save_image_by_thread(image_bg, out_path, save_mode="jpg", quality=None, dpi=None, _format="JPEG")
-                    # image_bg.save(out_path, format="JPEG")
                 elif out_pci_mode == ".png":
                     self.saver.save_image(
                         image=image_bg,
                         file_path=new_file_path,
-                        save_mode="jpg",
                         quality=100,
-                        dpi=(300, 300),
-                        _format="JPEG",
+                        dpi=(350, 350),
+                        _format="PNG",
                     )
                 else:
                     new_format = out_pci_mode.split(".")[-1]
@@ -727,17 +845,17 @@ class GeneratePic(object):
                         file_path=new_file_path,
                         save_mode=new_format,
                         quality=100,
-                        dpi=(300, 300),
+                        dpi=(350, 350),
                         _format=new_format,
                     )
-                    # save_image_by_thread(image_bg, out_path, save_mode="jpg", quality=100, dpi=(300, 300), _format="JPEG")
-                    # image_bg.save(out_path, quality=100, dpi=(300, 300), format="JPEG")
             else:
                 new_format = out_pci_mode.split(".")[-1]
                 self.saver.save_image(
                     image=image_bg,
                     file_path=new_file_path,
                     save_mode=new_format,
+                    quality=100,
+                    dpi=(350, 350),
                     _format=new_format,
                 )
                 # image_bg.save(out_path)

+ 173 - 1
python/service/image_deal_base_func.py

@@ -1,6 +1,6 @@
 import cv2
 import numpy as np
-from PIL import Image, ImageEnhance, ImageFilter, ImageOps
+from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageDraw, ImageChops, ImageStat
 import settings
 
 # 锐化图片
@@ -272,3 +272,175 @@ def calculate_average_brightness_opencv(img_gray, rows_to_check):
             print(f"警告:行号{row}超出图片范围,已跳过。")
 
     return brightness_list
+def get_extremes_from_transparent(img, alpha_threshold=10):
+    """
+    直接从透明图获取最左和最右的XY坐标
+    Args:
+        image_path: 透明图像路径
+        alpha_threshold: 透明度阈值,低于此值视为透明
+    Returns:
+        dict: 包含最左、最右坐标等信息
+    """
+    # 确保有alpha通道
+    if img.mode != 'RGBA':
+        img = img.convert('RGBA')
+
+    # 转换为numpy数组
+    img_array = np.array(img)
+
+    # 提取alpha通道
+    alpha = img_array[:, :, 3]
+
+    # 根据阈值创建mask
+    mask = alpha > alpha_threshold
+
+    if not np.any(mask):
+        print("警告: 没有找到非透明像素")
+        return None
+
+    # 获取所有非透明像素的坐标
+    rows, cols = np.where(mask)
+
+    if len(rows) == 0:
+        return None
+
+    # 找到最左和最右的像素
+    # 最左: 列坐标最小
+    leftmost_col = np.min(cols)
+    # 最右: 列坐标最大
+    rightmost_col = np.max(cols)
+
+    # 对于最左列,找到所有在该列的像素,然后取中间或特定位置的Y坐标
+    leftmost_rows = rows[cols == leftmost_col]
+    rightmost_rows = rows[cols == rightmost_col]
+
+    # 选择策略:可以取平均值、最小值、最大值或中位数
+    strategy = 'median'  # 可选: 'min', 'max', 'mean', 'median', 'top', 'bottom'
+
+    def get_y_coordinate(rows_values, strategy='median'):
+        if strategy == 'min':
+            return np.min(rows_values)
+        elif strategy == 'max':
+            return np.max(rows_values)
+        elif strategy == 'mean':
+            return int(np.mean(rows_values))
+        elif strategy == 'median':
+            return int(np.median(rows_values))
+        elif strategy == 'top':
+            return np.min(rows_values)
+        elif strategy == 'bottom':
+            return np.max(rows_values)
+        return int(np.median(rows_values))
+
+    # 获取最左点的Y坐标
+    leftmost_y = get_y_coordinate(leftmost_rows, strategy)
+    # 获取最右点的Y坐标
+    rightmost_y = get_y_coordinate(rightmost_rows, strategy)
+
+    result = {
+        'leftmost': (int(leftmost_col), int(leftmost_y)),
+        'rightmost': (int(rightmost_col), int(rightmost_y)),
+        'image_size': img.size,  # (width, height)
+        'alpha_threshold': alpha_threshold,
+        'pixel_count': len(rows),
+        'strategy': strategy
+    }
+
+    return result
+def create_polygon_mask_from_points(img, left_point, right_point):
+    """
+    根据两个点和图片边界创建多边形mask
+    形成四边形:图片左上角 → left_point → right_point → 图片右上角 → 回到左上角
+    Args:
+        left_point: (x, y) 左侧点
+        right_point: (x, y) 右侧点
+    Returns:
+        Image: 多边形mask
+        list: 多边形顶点坐标
+    """
+    # 打开图片获取尺寸
+    img_width, img_height = img.size
+
+    # 创建mask(全黑)
+    mask = Image.new('L', (img_width, img_height), 255)
+    draw = ImageDraw.Draw(mask)
+
+    # 定义多边形顶点(顺时针或逆时针顺序)
+    # 四边形:左上角 → left_point → right_point → 右上角
+    polygon_points = [
+        (-1, -1),  # 图片左上角
+        (-1, left_point[1]),  # 左侧点x=0
+        (left_point[0], left_point[1]),  # 左侧点
+        (right_point[0], right_point[1]),  # 右侧点
+        (img_width, right_point[1]),  # 右侧点y=0
+        (img_width, -1),  # 图片右上角
+    ]
+
+    # 绘制填充多边形
+    draw.polygon(polygon_points, fill=0, outline=255)
+
+    return mask
+
+
+def transparent_to_mask_pil(img, threshold=0, is_invert=False):
+    """
+    将透明图像转换为mask
+    threshold: 透明度阈值,低于此值的像素被视为透明
+    """
+    # 确保图像有alpha通道
+    if img.mode != 'RGBA':
+        img = img.convert('RGBA')
+    # 分离通道
+    r, g, b, a = img.split()
+    # 将alpha通道转换为二值mask
+    # 阈值处理:alpha值低于阈值的设为0(透明),否则设为255(不透明)
+    if is_invert is False:
+        mask = a.point(lambda x: 0 if x <= threshold else 255)
+    else:
+        mask = a.point(lambda x: 255 if x <= threshold else 0)
+    return mask
+
+# 两个MASK取交集
+def mask_intersection(mask1: Image.Image, mask2: Image.Image) -> Image.Image:
+    """
+    对两个 PIL mask 图像取交集(逻辑 AND)
+    - 输入:两个 mode='L' 的灰度图(0=假,非0=真)
+    - 输出:新的 mask,交集区域为 255,其余为 0(可选)
+    """
+    # 转为 numpy 数组
+    arr1 = np.array(mask1)
+    arr2 = np.array(mask2)
+
+    # 确保形状一致
+    assert arr1.shape == arr2.shape, "Mask shapes must match"
+
+    # 转为布尔:非零即 True
+    bool1 = arr1 > 0
+    bool2 = arr2 > 0
+
+    # 交集:逻辑与
+    intersection = bool1 & bool2
+
+    # 转回 uint8:True→255, False→0(标准 mask 格式)
+    result = (intersection * 255).astype(np.uint8)
+
+    return Image.fromarray(result, mode='L')
+
+def brightness_check(img_gray, mask):
+    img_gray = cv2_to_pil(img_gray)
+    img = Image.new("RGBA", img_gray.size, (255, 255, 255, 0))
+    img.paste(im=img_gray,mask=mask)
+    data = np.array(img)  # shape: (H, W, 4)
+    # 分离通道
+    r, g, b, a = data[..., 0], data[..., 1], data[..., 2], data[..., 3]
+    # 创建非透明掩码(Alpha > 0)
+    mask = a > 0
+    # 如果没有非透明像素,返回 0 或 NaN
+    if not np.any(mask):
+        return 0.0  # 或者 raise ValueError("No opaque pixels")
+    # 计算亮度(仅对非透明区域)
+    # 使用 ITU-R BT.601 标准权重
+    luminance = 0.299 * r[mask] + 0.587 * g[mask] + 0.114 * b[mask]
+
+    # 返回平均亮度
+    return float(np.mean(luminance))

+ 36 - 2
python/service/match_and_cutout_mode_control/base_deal_image_v2.py

@@ -181,10 +181,44 @@ class BaseDealImage(object):
             return {'code': 1, 'msg': '图片位置与顺序重复,请检查您的输入'}
 
         for val in imageOrderList:
-            if val not in ["正面", "侧视", "背面", "背侧", "组合", "组合2", "组合3", "组合4", "组合5"]:
+            image_orders = [
+                "俯视",
+                "侧视",
+                "后跟",
+                "鞋底",
+                "内里",
+                "组合",
+                "组合2",
+                "组合3",
+                "组合4",
+                "组合5",
+                "组合6",
+                "组合7",
+                "组合8",
+                "组合9",
+                "组合10",
+                "组合11",
+                "组合12",
+                "组合13",
+                "组合14",
+                "组合15",
+                "组合16",
+                "组合17",
+                "组合18",
+                "组合19",
+                "组合20",
+                "组合21",
+                "组合22",
+                "组合23",
+                "组合24",
+                "组合25",
+                "组合26",
+            ]
+            if val not in image_orders:
+                image_orders_str = ','.join(map(str, image_orders))
                 return {
                     "code": 1,
-                    "msg": "可选项为:正面,侧视,背面,背侧,组合,组合2,组合3,组合4,组合5",
+                    "msg": f"可选项为:{image_orders_str}",
                 }
 
         if resize_image_view not in imageOrderList:

+ 7 - 1
python/service/multi_threaded_image_saving.py

@@ -77,15 +77,21 @@ class ImageSaver:
         _format="JPEG",
         **kwargs,
     ):
+        print("保存图片:{}".format(out_path))
         if save_mode == "png":
-            image.save(out_path)
+            print("保存图片[png]:{}".format(out_path))
+            image.save(out_path,dpi=(350,350))
         else:
             if quality:
+                print("保存图片[quality]:{}".format(out_path))
                 if dpi:
+                    print("保存图片[dpi]:{}".format(out_path))
                     image.save(out_path, quality=quality, dpi=dpi, format=_format)
                 else:
+                    print("保存图片[not dpi]:{}".format(out_path))
                     image.save(out_path, quality=quality, format=_format)
             else:
+                print("保存图片[not quality]:{}".format(out_path))
                 image.save(out_path, format=_format)
         image.close()
 

+ 14 - 8
python/service/online_request/module_online_data.py

@@ -238,7 +238,7 @@ class AIGCDataRequest(object):
         """生成上脚图"""
         url = settings.DOMAIN + "/api/ai_image/main/upper_footer"
         resultData = self.s.post(url, data=data, headers=self.post_headers).json()
-
+        print("生成上脚图", resultData)
         code = resultData.get("code", 0)
         message = resultData.get("message", "")
         if code != 0:
@@ -720,7 +720,7 @@ class OnlineDataRequest(object):
                 for skuIdx, sku_data in enumerate(sku_list_basic):
                     sku_goods_art_no = sku_data.get("货号", "")
                     color_name = sku_data.get("颜色名称", "")
-                    size = sku_data.get("尺", 37)
+                    size = sku_data.get("尺", 37)
                     # 尺码
                     mainImages = sku_data.get("800x800", [])
                     if not mainImages:
@@ -728,7 +728,13 @@ class OnlineDataRequest(object):
                     success_goods_arts.append(sku_goods_art_no)
                     mainImagePath = mainImages[0]
                     imageUrl = self.uploadImage(local_path=mainImagePath)
+                    skuNameJson = []
+                    skuNameJson.append({"propName":"颜色","propValue":color_name})
+                    skuNameJson.append({"propName":"尺寸","propValue":size})
                     skuItemData = {
+                        "颜色": color_name,
+                        "尺寸": size,
+                        "skuPropName":color_name,
                         "skuNo": sku_goods_art_no,
                         "originalPrice": float(goods_price),
                         "newSkuWeight": int(1),
@@ -737,6 +743,7 @@ class OnlineDataRequest(object):
                         "sellingPrice": float(goods_price),
                         "quantity": int(quantity),
                         "showOrder": int(skuIdx + 1),
+                        "skuNameJson":skuNameJson
                     }
                     skuList.append(skuItemData)
                     itemImage = {
@@ -768,15 +775,14 @@ class OnlineDataRequest(object):
                 )
                 itemSkuImageList.append({
                         "propName": "尺寸",
-                        "value": None,
                         "isImageProp": 0,
                         "propShowOrder": 1,
                         "showOrder": 0,
-                        "propValue": size,
-                        "skuPropValueList": [
-                            {"propValue": str(37),
-                            "showOrder": 1}
-                            ],
+                        "propValue": str(size),
+                        "skuPropValueList":[{
+                            "propValue": str(size),
+                            "showOrder": 1,
+                            }]
                     })
                 detailImageUrl = self.uploadImage(local_path=detail_path)
                 category_info = "流行男鞋>>休闲鞋>>时尚休闲鞋"

+ 41 - 1
python/service/remove_bg_ali.py

@@ -145,7 +145,46 @@ class RemoveBgALi(object):
     def __init__(self):
         self.saver = ImageSaver()
         self.segment = Segment()
+    @func_set_timeout(40)
+    def get_image_cut_new(self, file_path, out_file_path=None, original_im=None):
+        if original_im:
+            original_pic = Picture(in_path=None, im=original_im)
+        else:
+            original_pic = Picture(file_path)
+
+        if original_pic.im.mode != "RGB":
+            original_pic.im = original_pic.im.convert("RGB")
 
+        new_pic = copy.copy(original_pic)
+        after_need_resize = False
+        if new_pic.x > new_pic.y:
+            if new_pic.x > 2000:
+                after_need_resize = True
+                new_pic.resize(2000)
+        else:
+            if new_pic.y > 2000:
+                after_need_resize = True
+                new_pic.resize_by_heigh(heigh=2000)
+
+        # new_pic.im.show()
+        body = self.segment.get_no_bg_goods(file_path=None, _im=new_pic.im)
+        body = eval(str(body))
+        try:
+            image_url = body["Data"]["ImageURL"]
+        except BaseException as e:
+            print("阿里抠图错误:", e)
+            # 处理失败,需要删除过程图片
+            return None
+        # 字节流转PIL对象
+        response = requests.get(image_url)
+        pic = response.content
+        _img_im = Image.open(BytesIO(pic))  # 阿里返回的抠图结果 已转PIL对象
+        box_size = _img_im.getbbox()
+        new_pp4_im = _img_im.crop(box_size)
+        byte_io = BytesIO()
+        new_pp4_im.save(byte_io, format='PNG')  # 将图像保存为 PNG 格式到 BytesIO 对象
+        byte_io.seek(0)  # 将指针重置到流的开头,以便后续读取
+        return byte_io
     @func_set_timeout(40)
     def get_image_cut(self, file_path, out_file_path=None, original_im=None):
         if original_im:
@@ -214,7 +253,8 @@ class RemoveBgALi(object):
             # _img_im.show("11111111111111111111111")
         if out_file_path:
             self.saver.save_image(
-                image=_img_im, file_path=out_file_path, save_mode="png"
+                image=_img_im, file_path=out_file_path,
+                quality=100,dpi=(350, 350), _format="PNG"
             )
             # _img_im.save(out_file_path)
         return _img_im

+ 65 - 12
python/service/remove_bg_pixian.py

@@ -4,7 +4,7 @@ from PIL import Image
 from .remove_bg_ali import RemoveBgALi
 import requests
 from io import BytesIO
-
+import settings
 
 class Segment(object):
     def __init__(self):
@@ -36,20 +36,42 @@ class Segment(object):
         else:
             return None, response.content
 
-    def get_no_bg_goods_by_url(self, url):
+    def get_no_bg_goods_by_url(self, url,key):
+        if key:
+            # 切换key
+            auth = key
+            # auth = (self.k, self.s)
+        else:
+            auth = (self.k, self.s)
         response = requests.post(
-            'https://api.pixian.ai/api/v2/remove-background',
+            'https://3api.valimart.net/api/v2/remove-background',
             data={
                 'image.url': url
             },
-            auth=(self.k, self.s)
+            auth=auth,
+            timeout=300
         )
 
-        if response.status_code == requests.codes.ok:
-            return response.content, ""
-        else:
-            print("response.status_code:", response.status_code)
-            return None, response.content
+        data = {"im": None,
+                "status_code": response.status_code, }
+
+        try:
+            if response.status_code == requests.codes.ok:
+                data["im"] = Image.open(BytesIO(response.content))
+                return data
+            else:
+                print("response.status_code:", response.status_code)
+                data = {"im": None,
+                        "status_code": "time_out",
+                        "message":"处理失败;{},{}".format(response.status_code,response.content)
+                        }
+                return data
+        except BaseException as e:
+            data = {"im": None,
+                    "status_code": "time_out",
+                    "message":"{}".format(e)
+                    }
+            return data
 
     def get_no_bg_goods(self, file_path=None, _im=None, key=None):
         im = _im
@@ -70,7 +92,7 @@ class Segment(object):
 
         try:
             response = requests.post(
-                'https://api.pixian.ai/api/v2/remove-background',
+                'https://3api.valimart.net/api/v2/remove-background',
                 files={'image': img},
                 data={
                     # Add more upload options here
@@ -150,9 +172,40 @@ class RemoveBgPiXian(object):
             return _img_im, None
         else:
             return None, _
-
+    def upload_image_by_io(self, image_pil:Image) -> str:
+        # post_headers = {"Authorization": settings.Authorization}
+        im = image_pil
+        img = BytesIO()
+        try:
+            im.save(img, format='JPEG')  # format: PNG or JPEG
+        except:
+            im.save(img, format='PNG')  # format: PNG or JPEG
+        img.seek(0)  # rewind to the start
+        try:
+            url = settings.DOMAIN + "/api/upload"
+            resultData = requests.post(
+                url, files={"file": img},
+                timeout=100
+            ).json()
+            return resultData["data"]["url"]
+        except Exception as e:
+            print("upload_image_by_io error:", e)
+            return None
     def run_by_image_im(self, im, key):
-        return self.segment.get_no_bg_goods(_im=im, key=key)
+        image_url = self.upload_image_by_io(im)
+        if image_url is None:
+            data = {"im": None,
+                        "status_code": "time_out",
+                        "message":"图片上传失败"
+                        }
+            return data
+        # image_url
+        # 把image_url中的ossimg.valimart.net
+        # 替换为img-cuts.valimart.net
+        print("image_url  1:", image_url)
+        image_url = image_url.replace('ossimg.valimart.net', 'img-cuts.valimart.net')
+        print("image_url  2:", image_url)
+        return self.segment.get_no_bg_goods_by_url(url=image_url,key=key)
 
     def get_image_cut(self, file_path, out_file_path=None, original_im=None, image_preprocessing=False, is_test=False):
         if original_im:

+ 49 - 34
python/service/run_main.py

@@ -15,7 +15,7 @@ from PIL import Image
 from io import BytesIO
 import os, re
 from functools import partial
-
+from service.customer_template_service import CustomerTemplateService
 # from multiprocessing import Process, Queue
 import pickle
 from .base_deal import BaseDealImage
@@ -390,7 +390,7 @@ class RunMain:
         config_data["sign_text"] = "已结束抠图处理"
         self.run_end_sign.emit(config_data)
 
-    def check_before_detail(self, config_data):
+    def check_before_detail(self, config_data,is_detail):
 
         # =============
         # 整体数据校验,返回错误内容,以及
@@ -472,16 +472,22 @@ class RunMain:
         print("temp_class====>", temp_name)
         # 获取所有文件夹基础数据内容  检查不满足要求的文件不满足要求移动到错误文件夹
         # 在访问 temp_class[temp_name].need_view 前增加检查
-        if temp_name not in temp_class or temp_class[temp_name] is None:
-            raise UnicornException(f"模板 {temp_name} 未正确初始化或不存在")
-
-        # 确保 temp_class[temp_name] 是可调用的
-        if not callable(temp_class[temp_name]):
-            raise UnicornException(f"模板 {temp_name} 不是有效的可调用对象")
-        try:
-          need_view_list = temp_class[temp_name].need_view
-        except KeyError as ke:
-          raise UnicornException("未选择详情页模板,请检查")
+        need_view_list = []
+        if is_detail == 1:
+            if temp_name not in temp_class or temp_class[temp_name] is None:
+                raise UnicornException(f"模板 {temp_name} 未正确初始化或不存在")
+            class_obj = temp_class[temp_name]
+            class_path = class_obj.get("cls")
+            template_type = class_obj.get("template_type")
+            # 确保 temp_class[temp_name] 是可调用的
+            # if template_type ==0:
+                # if not callable(temp_class[temp_name]):
+                #     raise UnicornException(f"模板 {temp_name} 不是有效的可调用对象")
+            try:
+                if template_type ==0:
+                    need_view_list = class_path.need_view
+            except KeyError as ke:
+                raise UnicornException("未选择详情页模板,请检查")
         _all_dir_info_data = get_all_dir_info_and_pic_info(
             image_dir, folder_name_list, need_view_list
         )
@@ -697,8 +703,6 @@ class RunMain:
                                 _goods_no_dict[goods_no] = value  # 需要生成的数据
                             finally_goods_no_need_temps[goods_no].append(__temp_name)
 
-            pass
-
         print("-----------------2goods_no_dict---------------")
         print(json.dumps(_goods_no_dict, ensure_ascii=False))
         print("-----------------2goods_no_dict---------------")
@@ -753,7 +757,7 @@ class RunMain:
             msg="开始处理抠图", goods_arts=goods_art_nos, status="开始处理",progress=progress
         )
     '''
-    def check_for_detail_first_call_back(self, data):
+    def check_for_detail_first_call_back(self, data,request_parmas):
         # 首次数据校验的信息返回
         # self.show_message(text="22222222222222222222222")
         # QMessageBox.critical(self, "警告", "1111111", QMessageBox.Ok)
@@ -824,7 +828,7 @@ class RunMain:
             # self.set_state(state_value=1)
             getAllData = data["data"]
             base_temp_name = getAllData["temp_name"]
-            set_temp_name = getAllData.get("template_name", "")
+            # set_temp_name = getAllData.get("template_name", "")
             kwargs = {
                 "config_data": config_data,
                 "_goods_no_dict": data["data"]["goods_no_dict"],
@@ -846,6 +850,7 @@ class RunMain:
                 assigned_page_dict=kwargs["assigned_page_dict"],
                 excel_temp_goods_no_data=kwargs["excel_temp_goods_no_data"],
                 finally_goods_no_need_temps=kwargs["finally_goods_no_need_temps"],
+                request_parmas=request_parmas,
             )
             # self._w_3 = WorkerOneThread(func=new_func, name="_w_3")
             # self._w_3.start()
@@ -865,6 +870,7 @@ class RunMain:
         assigned_page_dict,
         excel_temp_goods_no_data,
         finally_goods_no_need_temps,
+        request_parmas
     ):
         """
         excel_temp_goods_no_data: {},  # 表格数据可能存在多模板,数据结构为一个款号下的多个模板的数据列表
@@ -887,7 +893,6 @@ class RunMain:
             print(json.dumps(_goods_no_dict))
 
             print("==============_goods_no_dict  打印-end=================")
-
         all_detail_path_list = []
         out_put_dir_resp = ""
         detail_total_progress = len(finally_goods_no_need_temps.items())
@@ -974,6 +979,7 @@ class RunMain:
                         data={"goods_art_no": goods_no, "temp_name": _temp_name},
                     )
                 except BaseException as e:
+                    print("详情页打印输出错误信息===>",e)
                     detail_error_progress+=1
                     detail_progress = {"status":"处理失败","goods_art_no":goods_no, "current":detail_finish_progress, "total":detail_total_progress, "error":detail_error_progress,"folder":""}
                     self.sendAsyncMessage(
@@ -989,7 +995,6 @@ class RunMain:
                             "info": "款:{}生成详情异常:{}".format(goods_no, e),
                         }
                     )
-                    print(e)
                     # raise UnicornException("款:{}生成详情异常:{}".format(goods_no, e))
                     config_data["success_handler"].append(
                         {
@@ -1061,34 +1066,30 @@ class RunMain:
         target_error_folder,
         image_dir,
     ):
+        # 模板类型,0系统模板,1自定义模板
+        # 自定义模板数据
         # if self.windows.state == 99:
         #     self.show_progress_detail("用户主动取消:{}".format(goods_no))
         #     return
         self.show_progress_detail("正在生成:{},模板:{}".format(goods_no, temp_name))
         is_deal_success = False
+        class_obj = temp_class[temp_name]
+        class_path = class_obj.get("cls")
+        template_type = class_obj.get("template_type")
         print("=================deal_one_data=====================")
         print("goods_no", goods_no)
         print("模板:", temp_name)
         print("value:", value)
         print("temp_class:", temp_class)
+        print("class_obj:", class_obj)
         if temp_name not in temp_class or temp_class[temp_name] is None:
-            raise UnicornException(f"详情页模板 {temp_name} 未正确加载")
-            
-        if not callable(temp_class[temp_name]):
-            raise UnicornException(f"详情页模板 {temp_name} 不是有效的可调用对象")
-        if settings.IS_TEST:
-            temp_class[temp_name](
-                goods_no,
-                value,
-                out_put_dir=out_put_dir,
-                windows=self.windows,
-                assigned_page_list=assigned_page_list,
-            )
-            is_deal_success = True
-        else:
+                raise UnicornException(f"详情页模板 {temp_name} 未正确加载")
+        if template_type == 0:
+            # if not callable(class_path):
+            #     raise UnicornException(f"详情页模板 {temp_name} 不是有效的可调用对象")
             try:
                 # # 处理图片详情图生成
-                temp_class[temp_name](
+                class_path(
                     goods_no,
                     value,
                     out_put_dir=out_put_dir,
@@ -1110,7 +1111,21 @@ class RunMain:
                 raise UnicornException(
                     "{}处理失败,失败原因:{}".format(goods_no, error_text)
                 )
-
+        else:
+            try:
+                is_deal_success = True
+                service = CustomerTemplateService()
+                save_path = f"{out_put_dir}/详情图-{goods_no}"
+                print("传递的class信息====>",class_path)
+                service.generateTemplate(value,class_path,temp_name,save_path)
+            except BaseException as e:
+                self.show_progress_detail("{}处理失败".format(goods_no))
+                error_text = "{}".format(e)
+                print("error_text",error_text)
+                print(f"发生错误的行号: {e.__traceback__.tb_lineno}")
+                print(
+                    f"发生错误的文件: {e.__traceback__.tb_frame.f_globals['__file__']}"
+                )
         self.n += 1
 
         if not is_deal_success:

+ 76 - 3
python/settings.py

@@ -20,6 +20,8 @@ from databases import (
     batch_insert_device_configs,
     batch_insert_device_configsNew,
 )
+USER_TOKEN = None  # 可以通过外部设置或从配置文件中加载
+USER_ENV = None  # 可以通过外部设置或从配置文件中加载
 # 追加配置参数 machine_type 拍照机设备类型;0鞋;1服装
 MACHINE_TYPE = 1
 # 初始化数据表
@@ -211,7 +213,8 @@ DOMAIN = (
     if config.get("app", "env") != "dev"
     else "https://dev2.pubdata.cn"
 )
-
+def getDoman(env):
+    return "https://dev2.valimart.net" if env != "dev" else "https://dev2.pubdata.cn"
 
 Company = "惠利玛"
 
@@ -272,7 +275,11 @@ IMAGE_SAVE_MAX_WORKERS = int(
 COLOR_GRADATION_CYCLES = int(
     getSysConfigs("other_configs", "color_gradation_cycles", 23)
 )  # 色阶处理循环次数
-
+# grenerate_main_pic_brightness 亮度范围 默认值254 范围 200 -255 步长 1
+# opacity 透明度 默认值 0.5 范围 0.5-1 步长 0.1
+# IMAGE_MASK_CONFIG = int(
+#     getSysConfigs("basic_configs", "image_mask_config", {"mode":0,"opacity":0.5,"grenerate_main_pic_brightness":254})
+# )  # 色阶处理循环次数
 
 def recordDataPoint(token=None, page="", uuid=None, data=""):
     """记录日志"""
@@ -296,7 +303,30 @@ def recordDataPoint(token=None, page="", uuid=None, data=""):
     return requests.post(
         headers=headers, data=json.dumps(params), url=DOMAIN + "/api/record/point"
     )
-
+def syncPhotoRecord(data,action_type=0):
+    """同步图片记录"""
+    headers = {
+        "Authorization": f"Bearer {USER_TOKEN}",
+        "content-type": "application/json",
+    }
+    machine_type = MACHINE_TYPE
+    url = getDoman(USER_ENV) + f"/api/ai_image/camera_machine/photo_records_handler"
+    postData = {"data":data,"machine_type":machine_type,'action_type':action_type}
+    response = requests.post(url=url,data=json.dumps(postData, ensure_ascii=False, default=str), headers=headers)
+    print("用户token",response.content)
+def checkRecordSyncStatus():
+    # check_photo_record_sync_status
+    headers = {
+        "Authorization": f"Bearer {USER_TOKEN}",
+        "content-type": "application/json",
+    }
+    machine_type = MACHINE_TYPE
+    url = getDoman(USER_ENV) + f"/api/ai_image/camera_machine/photo_records_handler?machine_type={machine_type}"
+    try:
+        response = requests.get(url=url, headers=headers).json()
+        return response.get("data").get('status',True)
+    except Exception as e:
+        return True
 
 async def sendSocketMessage(
     code=0, msg="", data=None, device_status=2, msg_type=""
@@ -328,3 +358,46 @@ print("OUTPUT_DIR",__output_dir,OUTPUT_DIR)
 def handle_remove_readonly(func, path, exc):
     os.chmod(path, stat.S_IWRITE)
     func(path)
+
+
+CUSTOMER_TEMPLATE_URL = config.get("customer_template", "template_url")
+
+
+
+
+def hex_to_rgb(hex_color):
+    """
+    将十六进制颜色值转换为RGB颜色值
+    :param hex_color: 十六进制颜色值,例如 "#FF0000" 或 "FF0000"
+    :return: RGB元组,例如 (255, 0, 0)
+    """
+    # 移除可能存在的 # 符号
+    hex_color = hex_color.lstrip('#')
+    
+    # 确保是有效的十六进制字符串
+    if len(hex_color) != 6:
+        raise ValueError("十六进制颜色值格式不正确,应为6位十六进制数")
+    
+    # 将十六进制字符串转换为RGB值
+    try:
+        r = int(hex_color[0:2], 16)
+        g = int(hex_color[2:4], 16)
+        b = int(hex_color[4:6], 16)
+        return (r, g, b)
+    except ValueError:
+        return (255, 255, 255)
+
+
+def rgb_to_hex(r, g, b):
+    """
+    将RGB颜色值转换为十六进制颜色值
+    :param r: 红色分量 (0-255)
+    :param g: 绿色分量 (0-255)
+    :param b: 蓝色分量 (0-255)
+    :return: 十六进制颜色值,例如 "#FF0000"
+    """
+    # 验证RGB值在有效范围内
+    if not all(0 <= val <= 255 for val in [r, g, b]):
+        return "#FFFFFF"
+    
+    return "#{:02X}{:02X}{:02X}".format(r, g, b)

+ 38 - 8
python/sockets/message_handler.py

@@ -111,7 +111,20 @@ def handlerFolderDelete(limit_path, goods_art_no_arrays, is_write_txt_log):
                         logger.info(f"抠图前目录删除出现问题--Exception:{str(e)};{retry_count}")
                         time.sleep(0.5)  # 等待0.5秒后重试
     
-    return move_folder_array                
+    return move_folder_array
+def validate_goods_art_no(goods_art_no):
+        """
+        验证货号输入是否包含特殊字符
+        """
+        import re
+        # 定义不允许的特殊字符,主要是文件系统中可能导致路径问题的字符
+        invalid_chars = r'[<>:"/\\|?*]'
+        if re.search(invalid_chars, goods_art_no):
+            # 找出所有非法字符
+            invalid_found = re.findall(invalid_chars, goods_art_no)
+            invalid_str = ', '.join(set(invalid_found))
+            return False,f"货号包含非法字符: {invalid_str},请修改后再提交"
+        return True,""
 # socket消息发送逻辑处理方法
 async def handlerSend(
     manager: ConnectionManager,
@@ -161,8 +174,17 @@ async def handlerSend(
                 websocket_manager=manager, smart_shooter=smart_shooter
             )
             # 是否强制初始化
-            is_force_init = data.get("value", False)
+            is_force_init = False
             loop.create_task(device_ctrl.initDevice(is_force_init), name="init_mcu")
+        case "get_mcu_info":
+            device_ctrl = DeviceControl(
+                websocket_manager=manager, smart_shooter=smart_shooter
+            )
+            if not device_ctrl.is_running:
+                device_ctrl.sendSocketMessage(
+                    code=1, msg="mcu设备未连接,请先连接设备", device_status=0
+                )
+            device_ctrl.send_get_all_info_to_mcu()
         case "control_mcu":
             device_name = data.get("device_name")
             value = data.get("value")
@@ -199,6 +221,13 @@ async def handlerSend(
                 )
                 await manager.send_personal_message(data, websocket)
                 return
+            is_ok, msg = validate_goods_art_no(goods_art_no)
+            if not is_ok:
+                data = manager.jsonMessage(
+                    code=1, msg=msg, msg_type=msg_type
+                )
+                await manager.send_personal_message(data, websocket)
+                return
             session = SqlQuery()
             sys_configs = CRUD(SysConfigs)
             action_configs = sys_configs.read(
@@ -211,7 +240,7 @@ async def handlerSend(
             tab_id = action_configs_json.get(action_flag)
             photoRecord = CRUD(PhotoRecord)
             goods_art_record = photoRecord.read(
-                session, conditions={"goods_art_no": goods_art_no}
+                session, conditions={"goods_art_no": goods_art_no,"delete_time": None}
             )
             if goods_art_record != None:
                 data = manager.jsonMessage(
@@ -232,7 +261,8 @@ async def handlerSend(
                 await manager.send_personal_message(data, websocket, msg_type=msg_type)
                 return
             action_list = [dict(device.__dict__) for device in all_devices]
-            print("handl send smart_shooter", smart_shooter)
+            print("执行拍摄 打印动作列表", condtions)
+            # logger.info("执行拍摄 打印动作列表", json.dumps(action_list))
             device_ctrl = DeviceControl(
                 websocket_manager=manager, smart_shooter=smart_shooter
             )
@@ -263,7 +293,7 @@ async def handlerSend(
             )
             session = SqlQuery()
             crud = CRUD(PhotoRecord)
-            record = crud.read(session=session, order_by="id", ascending=False)
+            record = crud.read(session=session, order_by="id", ascending=False,conditions={"delete_time": None})
             if record == None:
             # 发送失败消息
                 data = manager.jsonMessage(
@@ -297,7 +327,7 @@ async def handlerSend(
             record_id = data.get("record_id")
             session = SqlQuery()
             photoRecord = CRUD(PhotoRecord)
-            goods_art_record = photoRecord.read(session, conditions={"id": record_id})
+            goods_art_record = photoRecord.read(session, conditions={"id": record_id,"delete_time": None})
             if goods_art_record == None:
                 data = manager.jsonMessage(
                     code=1,
@@ -485,7 +515,7 @@ async def handlerSend(
             goods_art_no = data.get("goods_art_no", "")
             session = SqlQuery()
             photoRecord = CRUD(PhotoRecord)
-            goods_art_record = photoRecord.read(session, conditions={"id": id})
+            goods_art_record = photoRecord.read(session, conditions={"id": id,"delete_time": None})
             if goods_art_record == None:
                 data = manager.jsonMessage(
                     code=1,
@@ -531,7 +561,7 @@ async def handlerSend(
             session = SqlQuery()
             for goods_art_no in goods_art_no_arrays:
                 pr = CRUD(PhotoRecord)
-                images = pr.read_all(session, conditions={"goods_art_no": goods_art_no})
+                images = pr.read_all(session, conditions={"goods_art_no": goods_art_no,"delete_time": None})
                 if not images:
                     data = manager.jsonMessage(
                         code=1,

+ 2 - 1
python/sockets/socket_server.py

@@ -28,11 +28,12 @@ async def updateDataRecord(PhotoFilename, id):
     # record_model = PhotoRecord(**data)
     session = SqlQuery()
     record_model = CRUD(PhotoRecord)
-    model = record_model.read(session, conditions={"id": id})
+    model = record_model.read(session, conditions={"id": id,"delete_time": None})
     if model == None:
         print(f"smart shooter 拍照记录更新失败,记录id:{id},不存在")
     else:
         # 走编辑逻辑
+        settings.syncPhotoRecord(data,action_type=3)
         record_model.updateConditions(session, conditions={"id": id}, **data)
         print(f"smart shooter 拍照记录更新成功,记录id:{id}")
     session.close()

+ 26 - 6
python/temp.py

@@ -1,6 +1,26 @@
-# from PIL import Image
-# from settings import recordDataPoint
-import time
-from service.online_request.module_online_data import OnlineDataRequest,AIGCDataRequest
-aigc = OnlineDataRequest("Bearer f99e72d818b504d23e0581ef1b1a2b4bb687c683")
-aigc.uploadGoods2ThirdParty("",["惠利玛@拼多多"])
+import zmq,json
+def __send_tcp_message(socket, msg):
+    socket.send_string(json.dumps(msg, ensure_ascii=False))
+    rep = socket.recv()
+    str_msg = rep.decode("utf-8")
+    json_msg = json.loads(str_msg)
+    return json_msg
+LISTEN_REQ = "tcp://127.0.0.1:54543"
+SET_REQ = "tcp://127.0.0.1:54544"
+context = zmq.Context()
+req_socket = context.socket(zmq.REQ)
+# 设置发送超时为 5000 毫秒(5 秒)
+req_socket.setsockopt(zmq.RCVTIMEO, 2 * 1000)
+# 设置接收超时为 5000 毫秒(5 秒)
+req_socket.setsockopt(zmq.SNDTIMEO, 2 * 1000)
+req_socket.setsockopt(zmq.LINGER, 0)  # 设置为 0 表示不等待未完成的操作
+req_socket.connect(SET_REQ)
+req = {}
+req["msg_type"] = "Request"
+req["msg_id"] = "GetCamera"
+req["msg_seq_num"] = 0
+req["CameraSelection"] = "Single"
+req["CameraKey"] = "Canon Inc.|Canon EOS 650D|12"
+json_msg = __send_tcp_message(req_socket,req)
+cameraInfo = json_msg.get("CameraInfo")
+print("cameraInfo",json_msg)

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff