Browse Source

Merge remote-tracking branch 'origin/multi_camera_version' into feature-frontend

panqiuyao 3 ngày trước cách đây
mục cha
commit
4720f5e0b8
64 tập tin đã thay đổi với 6430 bổ sung2244 xóa
  1. 338 0
      generateServer.API.md
  2. 1 1
      python/action.json
  3. 727 548
      python/api.py
  4. 18 0
      python/canvas_json.json
  5. 13 5
      python/config.ini
  6. 333 0
      python/conifg_info.py
  7. 89 20
      python/custom_plugins/plugins/detail_template/hongqingting/detail_hongqingting2.py
  8. 92 17
      python/custom_plugins/plugins/detail_template/hongqingting/detail_hongqingting3.py
  9. 18 12
      python/custom_plugins/plugins/detail_template/huilima/detail_huilima1.py
  10. 18 17
      python/custom_plugins/plugins/detail_template/huilima/detail_huilima2.py
  11. 18 12
      python/custom_plugins/plugins/detail_template/huilima/detail_huilima3.py
  12. 18 12
      python/custom_plugins/plugins/detail_template/huilima/detail_huilima4.py
  13. 18 16
      python/custom_plugins/plugins/detail_template/huilima/detail_huilima5.py
  14. 289 0
      python/custom_plugins/plugins/detail_template/qingyangyizhan/detail_qingyangyizhan2.py
  15. 160 148
      python/custom_plugins/plugins_mode/detail_generate_base.py
  16. 120 11
      python/databases.py
  17. 43 23
      python/detail_template_test.json
  18. 4 5
      python/detail_template_test.py
  19. 12 21
      python/detail_template_test_qingyangyizhan.json
  20. 18 16
      python/detail_template_test_xinnuo.json
  21. 35 0
      python/docs/socket命令.md
  22. 19 7
      python/mcu/BaseClass.py
  23. 3 3
      python/mcu/BlueToothMode.py
  24. 318 108
      python/mcu/DeviceControl.py
  25. 65 2
      python/mcu/LineControl.py
  26. 1 1
      python/mcu/Mcu.py
  27. 176 59
      python/mcu/ProgramItem.py
  28. 75 89
      python/mcu/RemoteControlV2.py
  29. 4 3
      python/mcu/SerialIns.py
  30. 324 186
      python/mcu/capture/smart_shooter_class.py
  31. 124 0
      python/mcu_test.py
  32. 131 0
      python/mcu_test_2.py
  33. 1 0
      python/middleware.py
  34. 2 0
      python/model/device_config.py
  35. 8 6
      python/model/photo_record.py
  36. 41 3
      python/models.py
  37. 40 3
      python/service/auto_deal_pics/base_deal.py
  38. 10 7
      python/service/base.py
  39. 186 87
      python/service/base_deal.py
  40. 391 0
      python/service/customer_template_service.py
  41. 49 21
      python/service/data.py
  42. 112 14
      python/service/deal_image.py
  43. 1 1
      python/service/deal_one_image.py
  44. 18 11
      python/service/generate_goods_no_detail_pic/data.py
  45. 6 8
      python/service/generate_main_image/grenerate_main_image_test.py
  46. 334 87
      python/service/grenerate_main_image_test.py
  47. 173 1
      python/service/image_deal_base_func.py
  48. 2 0
      python/service/image_pic_deal.py
  49. 44 4
      python/service/match_and_cutout_mode_control/base_deal_image_v2.py
  50. 1 1
      python/service/match_and_cutout_mode_control/module_matching_photos_v2.py
  51. 1 1
      python/service/matching_photos/module_matching_photos.py
  52. 10 1
      python/service/multi_threaded_image_saving.py
  53. 136 45
      python/service/online_request/module_online_data.py
  54. 41 1
      python/service/remove_bg_ali.py
  55. 65 12
      python/service/remove_bg_pixian.py
  56. 411 300
      python/service/run_main.py
  57. 158 35
      python/settings.py
  58. 10 2
      python/sockets/connect_manager.py
  59. 345 74
      python/sockets/message_handler.py
  60. 1 1
      python/sockets/socket_client.py
  61. 49 49
      python/sockets/socket_server.py
  62. 40 126
      python/temp.py
  63. 13 1
      python/utils/common.py
  64. 109 0
      python/utils/utils_func.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
+

+ 1 - 1
python/action.json

@@ -72,7 +72,7 @@
         "take_picture": true,
         "turntable_position": 100.0,
         "turntable_angle": 0.0,
-        "shoe_upturn": true,
+        "shoe_upturn": false,
         "pre_delay": 3.0,
         "after_delay": 0.0,
         "led_switch": false,

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 727 - 548
python/api.py


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 18 - 0
python/canvas_json.json


+ 13 - 5
python/config.ini

@@ -10,7 +10,7 @@ app_run=api:app
 # 端口号
 port=7074
 debug=false
-env=dev
+env=prod
 # 线程数
 works=1
 project=惠利玛
@@ -19,7 +19,7 @@ is_upload=true
 # 日志相关
 log_file_name=app.log
 #最大字节数
-max_bytes=102400
+max_bytes=1024000
 #备份数量
 backup_counts=3
 # 地址
@@ -43,6 +43,14 @@ right_foot_action_1 = 99
 right_foot_action_2 = 99
 stop = 9
 
-[camera_config]
-low_iso = 100
-high_iso = 6400
+; [camera_config]
+; low_iso = 100
+; high_iso = 6400
+[output_config]
+output_dir = ..\..\..\output\
+
+[customer_template]
+template_url = http://localhost:3001
+
+[scan_config]
+scan_dir = ..\..\..\scans\

+ 333 - 0
python/conifg_info.py

@@ -0,0 +1,333 @@
+class ConfigManager:
+    def __init__(self):
+        self.CONFIG_METADATA = {}
+        self.CONFIG_METADATA_BY_ADDR = {}
+
+        # 初始化配置项
+        self._init_config_keys()
+
+    def _set_key_float(self, _key, _addr, _tips="", _readonly=False):
+        self.CONFIG_METADATA[_key] = {
+            "addr": _addr,
+            "tips": _tips,
+            "readonly": _readonly,
+            "type": "float",
+        }
+        if _addr in self.CONFIG_METADATA_BY_ADDR:
+            print("{} 有重复".format(_addr))
+            raise Exception("地址重复")
+        self.CONFIG_METADATA_BY_ADDR[_addr] = _key
+
+    def _set_key_int(self, _key, _addr, _tips="", _readonly=False):
+        self.CONFIG_METADATA[_key] = {
+            "addr": _addr,
+            "tips": _tips,
+            "readonly": _readonly,
+            "type": "int",
+        }
+        if _addr in self.CONFIG_METADATA_BY_ADDR:
+            print("{} 有重复".format(_addr))
+            raise Exception("地址重复")
+        self.CONFIG_METADATA_BY_ADDR[_addr] = _key
+
+    def _init_config_keys(self):
+        # test keys
+        self.test_key1 = 0.5
+        self._set_key_float(_key="test_key1", _addr=1)
+        self.test_key2 = 1
+        self._set_key_int(_key="test_key2", _addr=2, _tips="测试-2")
+
+        # 升降机上次命令位置
+        self.camera_high_motor_target_value = 0
+        self._set_key_float(
+            _key="camera_high_motor_target_value",
+            _addr=3,
+            _tips="升降机当前位置",
+            _readonly=True,
+        )
+
+        # 相机角度上次命令位置
+        self.camera_steering_target_value = 0
+        self._set_key_float(
+            _key="camera_steering_target_value",
+            _addr=4,
+            _tips="相机角度上次命令位置",
+            _readonly=True,
+        )
+
+        # ---------------------50开始处理--------------------
+        # 升降机当前实时位置
+        self.camera_high_motor_current_value = 0
+        self._set_key_float(
+            _key="camera_high_motor_current_value",
+            _addr=51,
+            _tips="升降机当前实时位置",
+            _readonly=True,
+        )
+
+        # 相机角度当前实时位置
+        self.camera_steering_current_value = 0
+        self._set_key_float(
+            _key="camera_steering_current_value",
+            _addr=52,
+            _tips="相机角度当前实时位置",
+            _readonly=True,
+        )
+
+        # 转盘角度当前实时位置
+        self.turntable_steering_current_value = 0
+        self._set_key_float(
+            _key="turntable_steering_current_value",
+            _addr=53,
+            _tips="转盘角度当前实时位置",
+            _readonly=True,
+        )
+
+        # 转盘前后当前实时位置
+        self.move_turntable_steering_current_value = 0
+        self._set_key_float(
+            _key="move_turntable_steering_current_value",
+            _addr=54,
+            _tips="转盘前后当前实时位置",
+            _readonly=True,
+        )
+
+        # 翻板通讯是否正常
+        self.over_steering_is_online = 0
+        self._set_key_int(
+            _key="over_steering_is_online",
+            _addr=55,
+            _tips="翻板是否在线",
+            _readonly=True,
+        )
+
+        # --------------------------基础设置--------------从ID 100开始
+        # 自动关机时间;单位秒
+        self.auto_power_off_diff_time = 7200
+        self._set_key_int(
+            _key="auto_power_off_diff_time", _addr=101, _tips="自动关机时间单位秒"
+        )
+
+        # 自动去使能;单位秒 MOTOR_TO_DISABLE
+        self.auto_motor_disable_diff_time = 180
+        self._set_key_int(
+            _key="auto_motor_disable_diff_time", _addr=102, _tips="自动去使能时间单位秒"
+        )
+
+        # 220V继电器是否转换方向
+        self.relay_220v_is_change_dir = 0
+        self._set_key_int(
+            _key="relay_220v_is_change_dir", _addr=103, _tips="220V继电器是否转换方向"
+        )
+
+        # 伺服急停继电器是否转换方向
+        self.relay_motor_stop_is_change_dir = 0
+        self._set_key_int(
+            _key="relay_motor_stop_is_change_dir",
+            _addr=104,
+            _tips="伺服急停继电器是否转换方向",
+        )
+
+        # 刹车继电器是否转换方向
+        self.relay_brake_is_change_dir = 0
+        self._set_key_int(
+            _key="relay_brake_is_change_dir", _addr=105, _tips="刹车继电器是否转换方向"
+        )
+
+        # 增加升降的限位高度,单位mm
+        self.camera_high_motor_max_height = 387
+        self._set_key_int(
+            _key="camera_high_motor_max_height", _addr=106, _tips="升降的限位高度"
+        )
+
+        # 是否检查设备是否在线
+        self.is_check_motor_online = 1
+        self._set_key_int(
+            _key="is_check_motor_online", _addr=107, _tips="是否检查设备是否在线"
+        )
+
+        # 是否检查设备是否堵转
+        self.is_check_motor_blocked = 1
+        self._set_key_int(
+            _key="is_check_motor_blocked", _addr=108, _tips="是否检查设备是否堵转"
+        )
+
+        # 是否完成设备自动参数设定
+        self.has_been_set_motor_config = 0
+        self._set_key_int(
+            _key="has_been_set_motor_config",
+            _addr=109,
+            _tips="是否完成设备自动参数设定",
+        )
+
+        # 校准角度方向
+        self.angle_diff_dir = 0
+        self._set_key_int(_key="angle_diff_dir", _addr=110, _tips="校准角度方向")
+
+        # LED的默认亮度
+        self.led_default_brightness = 60
+        self._set_key_int(
+            _key="led_default_brightness", _addr=111, _tips="LED的默认亮度"
+        )
+
+        # 是否自动关闭使能
+        self.is_auto_motor_to_disable = 1
+        self._set_key_int(
+            _key="is_auto_motor_to_disable", _addr=112, _tips="是否自动关闭使能"
+        )
+
+        # 转盘转速比
+        self.turntable_steering_angle_ratio = 35.6
+        self._set_key_float(
+            _key="turntable_steering_angle_ratio", _addr=113, _tips="转盘转速比"
+        )
+
+        # LED灯的数量
+        self.led_count = 20
+        self._set_key_int(_key="led_count", _addr=114, _tips="LED灯的数量")
+
+        # 转盘前后移动设备的初始化模式 1表示碰撞  0表示限位
+        self.turntable_move_to_init_mode = 0
+        self._set_key_int(
+            _key="turntable_move_to_init_mode",
+            _addr=115,
+            _tips="转盘前后移动设备的初始化模式 1表示碰撞  0表示限位",
+        )
+
+        # 是否测试
+        self.is_test = 0
+        self._set_key_int(_key="is_test", _addr=116, _tips="is_test")
+
+        # 电机停止速度检测
+        self.low_speed = 300
+        self._set_key_int(_key="low_speed", _addr=117, _tips="电机停止速度检测")
+
+        # 自动发送基础信息
+        self.is_auto_send_base_info = 0
+        self._set_key_int(
+            _key="is_auto_send_base_info", _addr=118, _tips="自动发送基础信息"
+        )
+
+        # 失去使能升降自动归零
+        self.disable_camera_high_to_zero = 0
+        self._set_key_int(
+            _key="disable_camera_high_to_zero", _addr=119, _tips="失去使能升降自动归零"
+        )
+
+        # 前后移动电机初始化后距离
+        self.front_rear_motor_init_distance = 0
+        self._set_key_int(
+            _key="front_rear_motor_init_distance",
+            _addr=120,
+            _tips="前后移动电机初始化后距离",
+        )
+
+        # 每秒自动检查电机是否停止
+        self.auto_check_motor_stop = 0
+        self._set_key_int(
+            _key="auto_check_motor_stop", _addr=121, _tips="每秒自动检查电机是否停止"
+        )
+
+        # 前后移动电机是否存在
+        self.turntable_move_is_exist = 1
+        self._set_key_int(
+            _key="turntable_move_is_exist", _addr=122, _tips="前后移动电机是否存在"
+        )
+
+        # -------------------------------------
+        # 转盘偏移角度 单位1度
+        self.turntable_steering_deviation = 0
+        self._set_key_float(
+            _key="turntable_steering_deviation", _addr=200, _tips="转盘偏移角度"
+        )
+
+        # 相机偏移角度 单位1度
+        self.camera_steering_deviation = 0
+        self._set_key_float(
+            _key="camera_steering_deviation", _addr=201, _tips="相机偏移角度"
+        )
+
+        # 升降偏移距离mm
+        self.camera_high_motor_deviation = 0
+        self._set_key_int(
+            _key="camera_high_motor_deviation", _addr=202, _tips="升降偏移距离mm"
+        )
+
+        # 翻板中位 单位1度
+        self.overturn_steering_middle = 95
+        self._set_key_float(
+            _key="overturn_steering_middle", _addr=203, _tips="翻板中位 单位0.1度"
+        )
+
+        # 翻板高位 单位1度
+        self.overturn_steering_high = 150
+        self._set_key_float(
+            _key="overturn_steering_high", _addr=204, _tips="翻板高位 单位0.1度"
+        )
+
+        # 翻板上升速度
+        self.overturn_steering_up_speed = 2
+        self._set_key_int(
+            _key="overturn_steering_up_speed", _addr=205, _tips="翻板上升速度"
+        )
+
+        # 翻板下降速度
+        self.overturn_steering_down_speed = 4
+        self._set_key_int(
+            _key="overturn_steering_down_speed", _addr=206, _tips="翻板下降速度"
+        )
+
+        # 是否完成初始化
+        self.has_been_set = 0
+        self._set_key_int(
+            _key="has_been_set", _addr=207, _tips="是否完成初始化", _readonly=True
+        )
+
+        # 相机调焦功能支持
+        self.camera_has_focal = 0
+        self._set_key_int(_key="camera_has_focal", _addr=208, _tips="相机调焦功能支持")
+
+        # 相机最大焦段
+        self.camera_max_focal = 55
+        self._set_key_int(_key="camera_max_focal", _addr=209, _tips="相机最大焦段")
+
+        # 相机最小焦段
+        self.camera_min_focal = 15
+        self._set_key_int(_key="camera_min_focal", _addr=210, _tips="相机最小焦段")
+
+        # 相机焦段步进ratio
+        self.camera_focal_ratio = 0.0
+        self._set_key_float(
+            _key="camera_focal_ratio", _addr=211, _tips="相机焦段步进ratio"
+        )
+
+        # 相机焦段步进自动归零
+        self.camera_zoom_auto_to_zero = 1
+        self._set_key_float(
+            _key="camera_zoom_auto_to_zero", _addr=212, _tips="相机焦段步进自动归零"
+        )
+
+        # 相机焦段当前实时位置
+        self.camera_zoom_motor_current_value = 0
+        self._set_key_float(
+            _key="camera_zoom_motor_current_value",
+            _addr=213,
+            _tips="相机焦段当前实时位置",
+            _readonly=True,
+        )
+
+        # =====================================================
+        # 获取剩余内存
+        self.get_memory = 0
+        self._set_key_float(
+            _key="get_memory", _addr=300, _tips="获取剩余内存", _readonly=True
+        )
+
+        # =====================================================
+
+        # ===============调试用======================
+        # 相机焦段功能设置
+        self.camera_focal_set = 0
+        self._set_key_float(
+            _key="camera_focal_set", _addr=400, _tips="相机焦段功能设置", _readonly=True
+        )

+ 89 - 20
python/custom_plugins/plugins/detail_template/hongqingting/detail_hongqingting2.py

@@ -244,27 +244,33 @@ class DetailPicGet(DetailBase):
         return bg_img
     def deal_pic_6(self):
         if self.get_text_value("模特图"):
-            hh_img = PictureProcessing(r"{}\6.jpg".format(self.root))
-            mote_img = PictureProcessing(self.get_text_value("模特图"))
-            mote_img = mote_img.resize(value=896)
-            bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height+90), (252,238,225))
-            bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
-            return bg_img
+            try:
+                hh_img = PictureProcessing(r"{}\6.jpg".format(self.root))
+                mote_img = PictureProcessing(self.get_text_value("模特图"))
+                mote_img = mote_img.resize(value=896)
+                bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height+90), (252,238,225))
+                bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
+                bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
+                return bg_img
+            except:
+              return
         else:
             return
 
     def deal_pic_7(self):
         if self.get_text_value("场景图"):
-            hh_img = PictureProcessing(r"{}\7-1.jpg".format(self.root))
-            mote_img = PictureProcessing(self.get_text_value("场景图"))
-            mote_img = mote_img.resize(value=896)
-            ff_img = PictureProcessing(r"{}\7-2.jpg".format(self.root))
-            bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height+ff_img.height), (252,238,225))
-            bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
-            bg_img = bg_img.paste_img(top_img=ff_img,base="nc", value=(0, hh_img.height+mote_img.height))
-            return bg_img
+            try:
+                hh_img = PictureProcessing(r"{}\7-1.jpg".format(self.root))
+                mote_img = PictureProcessing(self.get_text_value("场景图"))
+                mote_img = mote_img.resize(value=896)
+                ff_img = PictureProcessing(r"{}\7-2.jpg".format(self.root))
+                bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height+ff_img.height), (252,238,225))
+                bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
+                bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
+                bg_img = bg_img.paste_img(top_img=ff_img,base="nc", value=(0, hh_img.height+mote_img.height))
+                return bg_img
+            except:
+              return
         else:
             return
     def deal_pic_8(self):
@@ -465,16 +471,17 @@ class DetailPicGet(DetailBase):
             pp_png_1 = pp_png_1.transpose()
             pp_jpg_1 = pp_jpg_1.transpose()
 
-
+        posy=2325
         if pp_jpg_1.height > pp_jpg_1.width:
             aheight =350
             # 等比计算目标宽度
             awidth = int(pp_jpg_1.width * aheight / pp_jpg_1.height)
         else:
             if pp_jpg_1.height > (pp_jpg_1.width/2):
-                awidth = int(pp_jpg_1.width * 0.13)
+                awidth = int(bg_img.width * 0.4)
             else:
-                awidth = int(pp_jpg_1.width * 0.30)
+                awidth = int(bg_img.width * 0.4)
+                posy=2380
 
 
         pp_jpg_1 = pp_jpg_1.resize(value=awidth)
@@ -483,7 +490,7 @@ class DetailPicGet(DetailBase):
             mode="pixel",
             top_img=pp_jpg_1,
             base="cs",
-            value=(-15, 2325),
+            value=(-15, posy),
             top_png_img=pp_png_1,
         )
 
@@ -1105,5 +1112,67 @@ class DetailPicGet(DetailBase):
 
     def deal_pic_12(self):
         # 背景图
+        goods_art_no_list = list(self.data.keys())
+        # 背景图
         bg_img = PictureProcessing(r"{}\12.jpg".format(self.root))
+
+        item_img = PictureProcessing("RGB", (404, 334), (247,247,247))
+        bg_img = bg_img.to_overlay_pic_advance(
+            mode="pixel",
+            top_img=item_img,
+            base="es",
+            value=(150, 180+2158),
+            top_png_img=item_img,
+        )
+
+
+        # item4
+        item_img = PictureProcessing("RGBA", (bg_img.width, 760), (255,255,255))
+
+
+
+        pp_jpg_1, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art_no_list[0],
+            name="内里",
+        )
+
+
+
+        aheight =100
+        # 等比计算目标宽度
+        awidth = int(pp_jpg_1.width * aheight / pp_jpg_1.height)
+        pp_jpg_1 = pp_jpg_1.resize(value=awidth)
+        pp_png_1 = pp_png_1.resize(value=awidth)
+
+        ty=0
+        if self.check_shoe_is_right_by_pixel(im=pp_png_1.im) == False:
+            pp_jpg_1 = pp_jpg_1.transpose()
+            pp_png_1 = pp_png_1.transpose()
+            ty=1
+
+        pp_jpg_1=pp_jpg_1.rotate_advance(doge=90,is_crop=False)
+        pp_png_1=pp_png_1.rotate_advance(doge=90,is_crop=False)
+
+
+        if ty==1:
+            pp_jpg_1 = pp_jpg_1.transpose()
+            pp_png_1 = pp_png_1.transpose()
+
+        bg_img = bg_img.to_overlay_pic_advance(
+            mode="pixel",
+            top_img=pp_png_1,
+            base="es",
+            value=(-60+50,50+2158),
+            top_png_img=pp_png_1,
+        )
+        pp_jpg_1 = pp_jpg_1.transpose()
+        pp_png_1 = pp_png_1.transpose()
+        bg_img = bg_img.to_overlay_pic_advance(
+            mode="pixel",
+            top_img=pp_png_1,
+            base="es",
+            value=(60+50, 50+2158),
+            top_png_img=pp_png_1,
+        )
         return bg_img

+ 92 - 17
python/custom_plugins/plugins/detail_template/hongqingting/detail_hongqingting3.py

@@ -355,25 +355,31 @@ class DetailPicGet(DetailBase):
 
     def deal_pic_6(self):
         if self.get_text_value("模特图"):
-            hh_img = PictureProcessing(r"{}\6.jpg".format(self.root))
-            mote_img = PictureProcessing(self.get_text_value("模特图"))
-            mote_img = mote_img.resize(value=1400)
-            bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
-            return bg_img
+            try:
+                hh_img = PictureProcessing(r"{}\6.jpg".format(self.root))
+                mote_img = PictureProcessing(self.get_text_value("模特图"))
+                mote_img = mote_img.resize(value=1400)
+                bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height), (255,255,255))
+                bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
+                bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
+                return bg_img
+            except:
+              return
         else:
             return
 
     def deal_pic_7(self):
         if self.get_text_value("场景图"):
-            hh_img = PictureProcessing(r"{}\7.jpg".format(self.root))
-            mote_img = PictureProcessing(self.get_text_value("场景图"))
-            mote_img = mote_img.resize(value=1400)
-            bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
-            return bg_img
+            try:
+                hh_img = PictureProcessing(r"{}\7.jpg".format(self.root))
+                mote_img = PictureProcessing(self.get_text_value("场景图"))
+                mote_img = mote_img.resize(value=1400)
+                bg_img = PictureProcessing("RGB", (hh_img.width, hh_img.height+mote_img.height), (255,255,255))
+                bg_img = bg_img.paste_img(top_img=hh_img,base="nc", value=(0, 0))
+                bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, hh_img.height))
+                return bg_img
+            except:
+              return
         else:
             return
 
@@ -399,9 +405,12 @@ class DetailPicGet(DetailBase):
             name="俯视",
         )
 
-        aheight =650
-        # 等比计算目标宽度
-        awidth = int(pp_jpg_1.width * aheight / pp_jpg_1.height)
+        if pp_jpg_1.height > pp_jpg_1.width:
+            aheight =650
+            # 等比计算目标宽度
+            awidth = int(pp_jpg_1.width * aheight / pp_jpg_1.height)
+        else:
+            awidth=round(0.52*bg_img.width)
 
         pp_jpg_1 = pp_jpg_1.resize(value=awidth)
         pp_png_1 = pp_png_1.resize(value=awidth)
@@ -977,6 +986,72 @@ class DetailPicGet(DetailBase):
         return bg_img
 
     def deal_pic_12(self):
+
+        goods_art_no_list = list(self.data.keys())
         # 背景图
         bg_img = PictureProcessing(r"{}\12.jpg".format(self.root))
+
+        item_img = PictureProcessing("RGB", (510, 440), (247,247,247))
+        bg_img = bg_img.to_overlay_pic_advance(
+            mode="pixel",
+            top_img=item_img,
+            base="es",
+            value=(190, 240),
+            top_png_img=item_img,
+        )
+
+
+        # item4
+        item_img = PictureProcessing("RGBA", (bg_img.width, 760), (255,255,255))
+
+
+
+        pp_jpg_1, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art_no_list[0],
+            name="内里",
+        )
+
+
+
+        aheight =170
+        # 等比计算目标宽度
+        awidth = int(pp_jpg_1.width * aheight / pp_jpg_1.height)
+        pp_jpg_1 = pp_jpg_1.resize(value=awidth)
+        pp_png_1 = pp_png_1.resize(value=awidth)
+
+        ty=0
+        if self.check_shoe_is_right_by_pixel(im=pp_png_1.im) == False:
+            pp_jpg_1 = pp_jpg_1.transpose()
+            pp_png_1 = pp_png_1.transpose()
+            ty=1
+
+        pp_jpg_1=pp_jpg_1.rotate_advance(doge=90,is_crop=False)
+        pp_png_1=pp_png_1.rotate_advance(doge=90,is_crop=False)
+
+
+        if ty==1:
+            pp_jpg_1 = pp_jpg_1.transpose()
+            pp_png_1 = pp_png_1.transpose()
+
+        bg_img = bg_img.to_overlay_pic_advance(
+            mode="pixel",
+            top_img=pp_png_1,
+            base="es",
+            value=(-100-0,30),
+            top_png_img=pp_png_1,
+        )
+        pp_jpg_1 = pp_jpg_1.transpose()
+        pp_png_1 = pp_png_1.transpose()
+        bg_img = bg_img.to_overlay_pic_advance(
+            mode="pixel",
+            top_img=pp_png_1,
+            base="es",
+            value=(100-0, 30),
+            top_png_img=pp_png_1,
+        )
+
+
+
+
         return bg_img

+ 18 - 12
python/custom_plugins/plugins/detail_template/huilima/detail_huilima1.py

@@ -133,22 +133,28 @@ class DetailPicGet(DetailBase):
 
     #模特图
     def deal_pic_1_2(self):
-        if self.get_text_value("模特图"):
-            mote_img = PictureProcessing(self.get_text_value("模特图"))
-            mote_img = mote_img.resize(value=1200)
-            bg_img = PictureProcessing("RGB", (mote_img.width, mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            model_image = pic_data.get("模特图",None)
+            if model_image:
+                scene_image_copy = self.concatAigcImage(model_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
     #场景图
     def deal_pic_1_3(self):
-        if self.get_text_value("场景图"):
-            mote_img = PictureProcessing(self.get_text_value("场景图"))
-            mote_img = mote_img.resize(value=1200)
-            bg_img = PictureProcessing("RGB", (mote_img.width, mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("场景图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
 

+ 18 - 17
python/custom_plugins/plugins/detail_template/huilima/detail_huilima2.py

@@ -207,27 +207,28 @@ class DetailPicGet(DetailBase):
 
     # 模特图
     def deal_pic_3_2(self):
-        if self.get_text_value("模特图"):
-            mote_img = PictureProcessing(self.get_text_value("模特图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing(
-                "RGB", (mote_img.width, mote_img.height), (255, 255, 255)
-            )
-            bg_img = bg_img.paste_img(top_img=mote_img, base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            model_image = pic_data.get("模特图",None)
+            if model_image:
+                scene_image_copy = self.concatAigcImage(model_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
-
     # 场景图
     def deal_pic_3_3(self):
-        if self.get_text_value("场景图"):
-            mote_img = PictureProcessing(self.get_text_value("场景图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing(
-                "RGB", (mote_img.width, mote_img.height), (255, 255, 255)
-            )
-            bg_img = bg_img.paste_img(top_img=mote_img, base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("场景图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
 

+ 18 - 12
python/custom_plugins/plugins/detail_template/huilima/detail_huilima3.py

@@ -216,22 +216,28 @@ class DetailPicGet(DetailBase):
 
     # 模特图
     def deal_pic_2_2(self):
-        if self.get_text_value("模特图"):
-            mote_img = PictureProcessing(self.get_text_value("模特图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing("RGB", (mote_img.width, mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("模特图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
     # 场景图
     def deal_pic_2_3(self):
-        if self.get_text_value("场景图"):
-            mote_img = PictureProcessing(self.get_text_value("场景图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing("RGB", (mote_img.width, mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("场景图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
 

+ 18 - 12
python/custom_plugins/plugins/detail_template/huilima/detail_huilima4.py

@@ -226,22 +226,28 @@ class DetailPicGet(DetailBase):
 
     # 模特图
     def deal_pic_3_2(self):
-        if self.get_text_value("模特图"):
-            mote_img = PictureProcessing(self.get_text_value("模特图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing("RGB", (mote_img.width, mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("模特图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
     # 场景图
     def deal_pic_3_3(self):
-        if self.get_text_value("场景图"):
-            mote_img = PictureProcessing(self.get_text_value("场景图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing("RGB", (mote_img.width, mote_img.height), (255,255,255))
-            bg_img = bg_img.paste_img(top_img=mote_img,base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("场景图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
 

+ 18 - 16
python/custom_plugins/plugins/detail_template/huilima/detail_huilima5.py

@@ -419,27 +419,29 @@ class DetailPicGet(DetailBase):
 
     # 模特图
     def deal_pic_6_2(self):
-        if self.get_text_value("模特图"):
-            mote_img = PictureProcessing(self.get_text_value("模特图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing(
-                "RGB", (mote_img.width, mote_img.height), (255, 255, 255)
-            )
-            bg_img = bg_img.paste_img(top_img=mote_img, base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("模特图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
 
     # 场景图
     def deal_pic_6_3(self):
-        if self.get_text_value("场景图"):
-            mote_img = PictureProcessing(self.get_text_value("场景图"))
-            mote_img = mote_img.resize(value=1600)
-            bg_img = PictureProcessing(
-                "RGB", (mote_img.width, mote_img.height), (255, 255, 255)
-            )
-            bg_img = bg_img.paste_img(top_img=mote_img, base="nc", value=(0, 0))
-            return bg_img
+        detailed_images = []
+        for pic_data in self.goods_no_value["货号资料"]:
+            scene_image = pic_data.get("场景图",None)
+            if scene_image:
+                scene_image_copy = self.concatAigcImage(scene_image,resize=1600)
+                if scene_image_copy:
+                    detailed_images.append(scene_image_copy)
+        if len(detailed_images) >0:
+            return PictureProcessing(im=self.add_pic(detailed_images))
         else:
             return
 

+ 289 - 0
python/custom_plugins/plugins/detail_template/qingyangyizhan/detail_qingyangyizhan2.py

@@ -0,0 +1,289 @@
+"""
+步骤:
+1、整理需要处理的款号图-输出款号图文件夹
+2、整理所有相关的图片作为素材图
+3、按要求进行拼接
+"""
+
+from ast import mod
+import os
+import settings
+
+from PIL import ImageFont
+
+# from module.view_control.generate_goods_no_detail_pic.detail_generate_base import DetailBase
+# from module.view_control.generate_goods_no_detail_pic.pic_deal import PictureProcessing
+is_test_plugins = False
+try:
+    is_test_plugins = settings.is_test_plugins
+except:
+    is_test_plugins = False
+
+if is_test_plugins:
+    from custom_plugins.plugins_mode.detail_generate_base import DetailBase
+    from custom_plugins.plugins_mode.pic_deal import PictureProcessing
+else:
+    from plugins_mode.detail_generate_base import DetailBase
+    from plugins_mode.pic_deal import PictureProcessing
+
+plugins_name = "详情模板"
+company_name_list = ["轻氧驿站"]
+template_name = "qingyangyizhan-2"
+# "俯视", "侧视", "后跟", "鞋底", "内里" 组合,组合2
+
+class DetailPicGet(DetailBase):
+    need_view = ["俯视", "侧视", "后跟", "鞋底"]
+    root = r"{}\resources\detail_temp\qingyangyizhan\2".format(os.getcwd())
+
+    def __init__(
+        self,
+        goods_no,
+        goods_no_value: dict,
+        out_put_dir,
+        windows=None,
+        test=False,
+        excel_data=None,
+        assigned_page_list=None,
+    ):
+        super().__init__(
+            goods_no,
+            goods_no_value,
+            out_put_dir,
+            windows=windows,
+            excel_data=excel_data,
+            assigned_page_list=assigned_page_list,
+        )
+        self.template_name = template_name
+
+        self.root = r"{}\resources\detail_temp\qingyangyizhan\2".format(os.getcwd())
+        print(f"run {template_name} ")
+        self.base_bg_color = (255, 255, 255)
+        self.base_bg_color_2 = (244, 242, 243)
+        self.deal_pic_func_list = [
+            self.deal_pic_1,
+            self.deal_pic_2,
+            self.deal_pic_5,
+            self.deal_pic_6,
+            self.deal_pic_6_1,
+            self.deal_pic_7,
+        ]
+
+        if test:
+            # pp = self.generate_font_list_to_pic()
+            # pp.im.save(r"C:\Users\gymmc\Desktop\细节图示例/字号.png")
+            # for k, v in self.goods_no_value.items():
+            #     print(k, v)
+            self.run_test()
+        else:
+            self.run_all()
+
+    def run_test(self):
+        detailed_images = []
+        detailed_images.append(self.deal_pic_1())
+        detailed_images.append(self.deal_pic_2())
+        detailed_images.append(self.deal_pic_5())
+        detailed_images.append(self.deal_pic_6())
+        detailed_images.append(self.deal_pic_6_1())
+        detailed_images.append(self.deal_pic_7())
+
+        img = self.add_pic(detailed_images)
+        img.save(r"{}/{}.jpg".format(self.out_put_dir, self.goods_no, format="JPEG"))
+        img.show()
+
+    # 直立-卷发棒
+    def deal_pic_1(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\1.jpg".format(self.root),1600)
+        pp_bg2 = PictureProcessing(r"{}\2.jpg".format(self.root),1600)
+        pp_bg2_1 = PictureProcessing(r"{}\3.jpg".format(self.root),1600)
+        pp_bg2_2 = PictureProcessing(r"{}\4.jpg".format(self.root),1600)
+        pp_bg3 = PictureProcessing(r"{}\5.jpg".format(self.root),1600)
+        detailed_images.append(pp_bg)
+        detailed_images.append(pp_bg2)
+        detailed_images.append(pp_bg2_1)
+        detailed_images.append(pp_bg2_2)
+        goods_art_no_list = list(self.data.keys())
+        _, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art_no_list[0],
+            name="鞋底",
+        )
+        pp_png_1 = pp_png_1.resize(value=pp_bg3.width/3)
+        pp_bg3 = pp_bg3.paste_img(top_img=pp_png_1, base="cc", value=(0, -30))
+        detailed_images.append(pp_bg3)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+
+    # 展示鞋头和后跟
+    def deal_pic_2(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\6.jpg".format(self.root), 1600)
+        pp_bg_1 = PictureProcessing(r"{}\7.jpg".format(self.root), 1600)
+        pp_bg_2 = PictureProcessing(r"{}\8.jpg".format(self.root), 1600)
+        pp_bg5 = PictureProcessing(r"{}\9.jpg".format(self.root), 1600)
+        # pp_bg_special_bg = PictureProcessing(r"{}\special_bg.png".format(self.root), 1600)
+        # pp_bg_special_text = PictureProcessing(r"{}\special_bottom.png".format(self.root), 1600)
+        # goods_art_no_list = list(self.data.keys())
+        # _, pp_png_1 = self.image_one_pic(
+        #     return_orign=True,
+        #     goods_art_no=goods_art_no_list[0],
+        #     name="鞋底",
+        # )
+        # pp_bg5 = PictureProcessing(
+        #     "RGB", (pp_bg.width,pp_bg_special_bg.height), (255, 255, 255, 255)
+        # )
+        # pp_bg5 = pp_bg5.paste_img(top_img=pp_bg_special_bg, base="cc", value=(0, 0))
+        # product_past_canvas = PictureProcessing(
+        #     "RGBA", (pp_bg.width, pp_png_1.height/2), (255, 255, 255, 0)
+        # )
+        # # pp_jpg_1 = pp_jpg_1.resize(value=pp_bg.width)
+        # pp_png_1 = pp_png_1.crop(mode="min")
+        # pp_png_1 = pp_png_1.resize(value=pp_bg.width/2)
+        # product_past_canvas = product_past_canvas.paste_img(top_img=pp_png_1, base="nc", value=(0, -100))
+        # # product_past_canvas = product_past_canvas.rotate(doge=-100)
+        # pp_bg5 = pp_bg5.paste_img(top_img=product_past_canvas, base="nc", value=(0, 0))
+        # pp_bg5 = pp_bg5.paste_img(top_img=pp_bg_special_text, base="cs", value=(0, 0))
+        detailed_images.append(pp_bg)
+        detailed_images.append(pp_bg_1)
+        detailed_images.append(pp_bg_2)
+        detailed_images.append(pp_bg5)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+
+    # 展示鞋头放大图
+    def deal_pic_3(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\6.jpg".format(self.root), 1600)
+        detailed_images.append(pp_bg)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+
+    # 展示后跟放大图
+    def deal_pic_4(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\10.jpg".format(self.root))
+        goods_art_no_list = list(self.data.keys())
+        _, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art_no_list[0],
+            name="俯视",
+        )
+        product_past_canvas = PictureProcessing(
+            "RGBA", (pp_bg.width*1.5, pp_png_1.height*1.5 + 350), (255, 255, 255, 0)
+        )
+        pp_png_1 = pp_png_1.crop(mode="min")
+        pp_png_1 = pp_png_1.resize(value=pp_bg.width*3)
+        product_past_canvas = product_past_canvas.paste_img(top_img=pp_png_1, base="cc", value=(0, 0))
+        product_past_canvas = product_past_canvas.rotate(doge=-105)
+        pp_bg = pp_bg.paste_img(top_img=product_past_canvas, base="cc", value=(100,50))
+        detailed_images.append(pp_bg)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+
+    # 展示后跟细节卡片
+    def deal_pic_5(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\10.jpg".format(self.root))
+        goods_art_no_list = list(self.data.keys())
+        _, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art_no_list[0],
+            name="俯视",
+        )
+        product_past_canvas = PictureProcessing(
+            "RGBA", (pp_bg.width*2, pp_png_1.height*2 + 350), (255, 255, 255, 0)
+        )
+        pp_png_1 = pp_png_1.resize(value=pp_bg.width*1.4)
+        product_past_canvas = product_past_canvas.paste_img(top_img=pp_png_1, base="cc", value=(0, 0))
+        product_past_canvas =product_past_canvas.rotate(doge=-95)
+        pp_bg = pp_bg.paste_img(top_img=product_past_canvas, base="cc", value=(-130, 130))
+        detailed_images.append(pp_bg)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+
+    # "心动"设计亮点,展示旋转的侧视图
+    def deal_pic_6(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\11.jpg".format(self.root))
+        pp_bg1 = PictureProcessing(r"{}\12.jpg".format(self.root))
+        pp_bg2 = PictureProcessing(r"{}\13.jpg".format(self.root))
+        detailed_images.append(pp_bg)
+        detailed_images.append(pp_bg1)
+        detailed_images.append(pp_bg2)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+    def deal_pic_6_1(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\14.jpg".format(self.root))
+        pp_bg1 = PictureProcessing(r"{}\15.jpg".format(self.root))
+        goods_art_no_list = list(self.data.keys())
+        _, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art_no_list[0],
+            name="鞋底",
+        )
+        product_past_canvas = PictureProcessing(
+            "RGBA", (pp_bg.width*2, pp_png_1.height*2 + 350), (255, 255, 255, 0)
+        )
+        pp_png_1 = pp_png_1.resize(value=pp_bg.width/3.8)
+        product_past_canvas = product_past_canvas.paste_img(top_img=pp_png_1, base="cc", value=(0, 0))
+        product_past_canvas =product_past_canvas.rotate(doge=-8)
+        pp_bg = pp_bg.paste_img(top_img=product_past_canvas, base="cc", value=(0, 150))
+        detailed_images.append(pp_bg)
+        detailed_images.append(pp_bg1)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+    # 产品展示,帮面等等
+    def deal_pic_7(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\16.jpg".format(self.root))
+        goods_art_no_list = list(self.data.keys())
+        pp_jpg_1, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art_no_list[0],
+            name="俯视",
+        )
+        pp_png_1 = pp_png_1.resize(value=pp_bg.width*1.1)
+        # pp_jpg_1 = pp_jpg_1.resize(value=pp_bg.width)
+        # pp_bg = pp_bg.to_overlay_pic_advance(top_img=pp_jpg_1,top_png_img=pp_png_1, base="cc", value=(0, 0))
+        product_past_canvas = PictureProcessing(
+            "RGBA", (pp_bg.width*3, pp_png_1.height*3 + 350), (255, 255, 255, 0)
+        )
+        # pp_png_1 = pp_png_1.resize(value=pp_bg.width/3.8)
+        product_past_canvas = product_past_canvas.paste_img(top_img=pp_png_1, base="cc", value=(0, 0))
+        product_past_canvas =product_past_canvas.rotate(doge=-86)
+        pp_bg = pp_bg.paste_img(top_img=product_past_canvas, base="cc", value=(0, 70))
+        detailed_images.append(pp_bg)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+
+    # 功能展示与卖点
+    def deal_pic_8(self):
+        detailed_images = []
+        pp_bg = PictureProcessing(r"{}\10.jpg".format(self.root))
+        goods_art_no_list = list(self.data.keys())
+        goods_art = goods_art_no_list[0]
+        _, pp_png_1 = self.image_one_pic(
+            return_orign=True,
+            goods_art_no=goods_art,
+            name="内里",
+        )
+        pp_png_1 = pp_png_1.crop(mode="min")
+        pp_png_1 = pp_png_1.resize(value=300)
+        desc_title_text_bg = PictureProcessing(
+            "RGB", (342, 268), (255, 255, 255, 0)
+        )
+        desc_title_text_bg = desc_title_text_bg.paste_img(
+            mode="pixel",
+            top_img=pp_png_1,
+            base="cc",
+            value=(0, 0),
+        )
+        # desc_title_text_bg = desc_title_text_bg.rotate(doge=150)
+        # desc_title_text_bg = desc_title_text_bg.crop(mode="min")
+        desc_title_text_bg = desc_title_text_bg.radius(value=30)
+        pp_bg.paste_img(
+            mode="pixel",
+            top_img=desc_title_text_bg,
+            base="en",
+            value=(62, 746),
+        )
+        detailed_images.append(pp_bg)
+        return PictureProcessing(im=self.add_pic(detailed_images))
+
+    # 添加尺码表
+    def deal_pic_9(self):
+        image_path = r"{}\11.jpg".format(self.root)
+        return PictureProcessing(image_path)

+ 160 - 148
python/custom_plugins/plugins_mode/detail_generate_base.py

@@ -19,8 +19,8 @@ from natsort import ns, natsorted
 import threading
 from concurrent.futures import ThreadPoolExecutor
 from concurrent.futures import TimeoutError as THTimeoutError
-
-
+from middleware import UnicornException
+from logger import logger
 # import math
 from PIL import ImageFont
 import settings
@@ -50,28 +50,29 @@ def _init_message_thread():
 _init_message_thread()
 
 
-def sendMessageAsync(code=0, msg="开始处理详情", data=None, msg_type="detail_progress"):
-    """异步发送消息"""
-
-    def _send_in_thread():
-        # 在消息线程中调度任务
-        future = asyncio.run_coroutine_threadsafe(
-            sendSocketMessage(
-                code=code,
-                msg=msg,
-                data=data,
-                msg_type=msg_type,
-            ),
-            _message_loop,
-        )
-        # 可选:等待结果或设置超时
-        try:
-            result = future.result(timeout=5.0)
-        except THTimeoutError:
-            print("消息发送超时")
-
-    # 在线程中执行异步任务的调度
-    _send_in_thread()
+# def sendMessageAsync(code=0, msg="开始处理详情", data=None, msg_type="detail_progress",progress=None):
+#     """异步发送消息"""
+#     if progress is None:
+#         data["progress"] = progress
+#     def _send_in_thread():
+#         # 在消息线程中调度任务
+#         future = asyncio.run_coroutine_threadsafe(
+#             sendSocketMessage(
+#                 code=code,
+#                 msg=msg,
+#                 data=data,
+#                 msg_type=msg_type,
+#             ),
+#             _message_loop,
+#         )
+#         # 可选:等待结果或设置超时
+#         try:
+#             result = future.result(timeout=5.0)
+#         except THTimeoutError:
+#             print("消息发送超时")
+
+#     # 在线程中执行异步任务的调度
+#     _send_in_thread()
 
 
 class DetailBase(object):
@@ -144,30 +145,45 @@ class DetailBase(object):
             return False
 
     def del_detail_folder(self):
-        out_path = "{out_put_dir}/{goods_no}".format(
-            out_put_dir=self.out_put_dir, goods_no=self.goods_no
+        out_path = "{out_put_dir}/切片图-{template_name}".format(
+            out_put_dir=self.out_put_dir,template_name=self.template_name
+        )
+        detail_image_path = "{out_put_dir}/详情页-{template_name}".format(
+            out_put_dir=self.out_put_dir,template_name=self.template_name
         )
         if not os.path.exists(out_path):
             return
         try:
-            shutil.rmtree(out_path)
+            shutil.rmtree(out_path,onerror=settings.handle_remove_readonly)
         except BaseException as e:
             print("删除文件夹失败", e)
+    def del_detail_longimage(self):
+        detail_image_path = "{out_put_dir}/详情页-{template_name}".format(
+            out_put_dir=self.out_put_dir,template_name=self.template_name
+        )
+        if not os.path.exists(detail_image_path):
+            return
+        try:
+            shutil.rmtree(detail_image_path)
+        except BaseException as e:
+            print("删除详情页失败", e)
+            logger.info(f"detail_generae_base 抠图前目录删除出现问题:{str(e)}")
 
     def run_all(self):
         if self.template_name:
-            self.out_put_dir = "{}/详情模板{}".format(
-                self.out_put_dir, self.template_name
+            self.out_put_dir = "{}/详情图-{}".format(
+                self.out_put_dir, self.goods_no
             )
 
         print("===================detailed_images=================")
         # 如果没有指定页面,则删除指定目录下的对应的详情文件夹
         if not self.assigned_page_list:
             self.del_detail_folder()
+            self.del_detail_longimage()
         detailed_images = self.deal_details()
         self.create_folder(self.out_put_dir)
-        detail_path = "{out_put_dir}/{goods_no}/详情页切片".format(
-            out_put_dir=self.out_put_dir, goods_no=self.goods_no
+        detail_path = "{out_put_dir}/切片图-{template_name}".format(
+            out_put_dir=self.out_put_dir, goods_no=self.goods_no,template_name=self.template_name
         )
         self.create_folder(detail_path)
         self.save_to_png(detailed_images=detailed_images, detail_path=detail_path)
@@ -188,57 +204,45 @@ class DetailBase(object):
             else:
                 if "主图" in self.assigned_page_list:
                     self.deal_all_main_pic()
-        # ----------如果是红蜻蜓则创建同颜色下的其他货号颜色文件夹---------------
-        if settings.PROJECT == "红蜻蜓":
-            if "data_all_goods_art_info" in self.goods_no_value:
-                # 数据格式:[{'number': '14250232', 'goods_art_no': 'AC52001173', 'color': '杏色'}, ]
-                for pic_data in self.goods_no_value["货号资料"]:
-                    if "颜色名称" not in pic_data:
-                        continue
-                    color_name = pic_data["颜色名称"]
-
-                    color_file_path = "{out_put_dir}/{goods_no}/{goods_number}".format(
-                        out_put_dir=self.out_put_dir,
-                        goods_no=self.goods_no,
-                        goods_number=pic_data["编号"],
-                    )
-                    for i in self.goods_no_value["data_all_goods_art_info"]:
-                        if color_name in i["color"]:
-                            new_path = "{out_put_dir}/{goods_no}/{goods_number}".format(
-                                out_put_dir=self.out_put_dir,
-                                goods_no=self.goods_no,
-                                goods_number="NUM{}".format(i["number"]),
-                            )
-                            if not os.path.exists(new_path):
-                                # 创建文件夹
-                                os.makedirs(new_path)
-                                self.move_one_pic(
-                                    color_file_path,
-                                    new_path,
-                                    "NUM{}".format(i["number"]),
-                                )
-        sendMessageAsync(
-            code=0,
-            msg="详情页生成完成",
-            msg_type="detail_progress",
-            data={
-                "goods_no": self.goods_no,
-                "temp_name": self.template_name,
-                "status": "已完成",
-                "goods_art_nos": self.goods_art_nos,
-            },
-        )
-        scp_path = "{out_put_dir}/{goods_no}".format(
-            out_put_dir=self.out_put_dir, goods_no=self.goods_no
+        scp_path = "{out_put_dir}".format(
+            out_put_dir=self.out_put_dir
         )
-        if self.get_text_value("模特图"):
-            model_pic = self.get_text_value("模特图")
-            shutil.copy(model_pic, f"{scp_path}/模特图.jpg")
-        if self.get_text_value("场景图"):
-            scene_pic = self.get_text_value("场景图")
-            shutil.copy(scene_pic, f"{scp_path}/场景图.jpg")
+        for pic_data in self.goods_no_value["货号资料"]:
+            print("正在复制 pic_data",pic_data)
+            scene_image = pic_data.get("场景图",None)
+            model_image = pic_data.get("模特图",None)
+            goods_art_no = pic_data.get("货号",None)
+            print("正在复制 scene_image",scene_image)
+            print("正在复制 model_image",model_image)
+            print("正在复制===================================>")
+            # {out_put_dir}/{goods_number}
+            # 资料长度,决定是否添加货号后缀
+            goods_art_lens = len(self.goods_no_value["货号资料"])
+            concat_shuffix = "" if goods_art_lens == 1 else f"_{goods_art_no}"
+            if model_image:
+                self.copyImage(model_image, f"{scp_path}/模特图{concat_shuffix}.jpg")
+            if scene_image:
+                self.copyImage(scene_image, f"{scp_path}/场景图{concat_shuffix}.jpg")
         return True
-
+    def copyImage(self,src_path,limit_path):
+        try:
+          shutil.copy(src_path, limit_path)
+        except Exception as e:
+            logger.info(f"copyImage 复制模特图/场景图出错:{str(e)}",src_path,limit_path)
+    def concatAigcImage(self,image_path,resize=1600,bg_color=(255,255,255)):
+        """拼接模特图场景图"""
+        try:
+                mote_img = PictureProcessing(image_path)
+                mote_img = mote_img.resize(value=resize)
+                bg_img = PictureProcessing(
+                    "RGB", (mote_img.width, mote_img.height), bg_color
+                )
+                bg_img = bg_img.paste_img(top_img=mote_img, base="nc", value=(0, 0))
+                print("拼接模特图场景图====>",image_path)
+                return bg_img
+        except Exception as e:
+             logger.info(f"copyImage 拼接模特图/场景图出错:{str(e)}")
+             return
     # 移动一张图片到新的文件夹
     def move_one_pic(self, old_path, new_path, new_name):
         image_file = os.listdir(old_path)[0]
@@ -249,46 +253,40 @@ class DetailBase(object):
 
     # 生成各个详情图切片
     def deal_details(self):
-        detailed_images = []
-        sendMessageAsync(
-            code=0,
-            msg="正在生成详情页切片",
-            msg_type="detail_progress",
-            data={
-                "goods_no": self.goods_no,
-                "temp_name": self.template_name,
-                "status": "进行中",
-                "goods_art_nos": self.goods_art_nos,
-            },
-        )
-        for index, func in enumerate(self.deal_pic_func_list):
-            image_pp = func()
-            if not self.assigned_page_list:
-                self.image_list_append(detailed_images, image_pp)
-            else:
-                index = "{}".format(index + 1)
-                if index in self.assigned_page_list:
+        try:
+            detailed_images = []
+            for index, func in enumerate(self.deal_pic_func_list):
+                image_pp = func()
+                if not self.assigned_page_list:
                     self.image_list_append(detailed_images, image_pp)
                 else:
-                    self.image_list_append(detailed_images, {"mes": "不生成"})
+                    index = "{}".format(index + 1)
+                    if index in self.assigned_page_list:
+                        self.image_list_append(detailed_images, image_pp)
+                    else:
+                        self.image_list_append(detailed_images, {"mes": "不生成"})
 
-        return [x for x in detailed_images if x]
+            return [x for x in detailed_images if x]
+        except KeyError as e:
+          raise UnicornException(f"缺少详情页资料:[{e}],请检查系统商品信息或excel是否缺少该字段")
+        except Exception as e:
+          raise UnicornException(str(e))
 
     # 生成拼接的图片
     def generate_spliced_picture(self):
-        sendMessageAsync(
-            code=0,
-            msg="正在生成详情拼接图",
-            msg_type="detail_progress",
-            data={
-                "goods_no": self.goods_no,
-                "temp_name": self.template_name,
-                "status": "进行中",
-                "goods_art_nos": self.goods_art_nos,
-            },
-        )
-        detail_path = "{out_put_dir}/{goods_no}/详情页切片".format(
-            out_put_dir=self.out_put_dir, goods_no=self.goods_no
+        # sendMessageAsync(
+        #     code=0,
+        #     msg="正在生成详情拼接图",
+        #     msg_type="detail_progress",
+        #     data={
+        #         "goods_no": self.goods_no,
+        #         "temp_name": self.template_name,
+        #         "status": "进行中",
+        #         "goods_art_nos": self.goods_art_nos,
+        #     },
+        # )
+        detail_path = "{out_put_dir}/切片图-{template_name}".format(
+            out_put_dir=self.out_put_dir, goods_no=self.goods_no,template_name=self.template_name
         )
         if not os.path.exists(detail_path):
             return
@@ -297,8 +295,8 @@ class DetailBase(object):
             detailed_images.append(PictureProcessing(image_data["file_path"]))
         # 生成拼接图
         img = self.add_pic(detailed_images)
-        join_path = "{out_put_dir}/{goods_no}/详情页".format(
-            out_put_dir=self.out_put_dir, goods_no=self.goods_no
+        join_path = "{out_put_dir}/详情页-{template_name}".format(
+            out_put_dir=self.out_put_dir, goods_no=self.goods_no,template_name=self.template_name
         )
         # self.create_folder(join_path)
         img.save("{}.jpg".format(join_path), format="JPEG")
@@ -360,35 +358,39 @@ class DetailBase(object):
             }
 
     def get_text_value(self, key, subsection_len=0):
-        text = ""
-        if key in self.goods_no_value:
-            if self.goods_no_value[key]:
-                text = str(self.goods_no_value[key])
-                text = text.replace(r"\n", "\n")
-
-        # if key in ["跟高", "鞋宽", "帮高", "脚掌围", "鞋长"]:
-        #     if text:
-        #         text = text.split(".")[0]
-
-        if subsection_len != 0:
-            text = text.split("\n")
-            text = [x for x in text if x]
-            if len(text) == 2:
-                text_1 = text[0]
-                text_2 = text[1]
-                return text_1, text_2
-            else:
-                if text:
+        try:
+            text = ""
+            if key in self.goods_no_value:
+                if self.goods_no_value[key]:
+                    text = str(self.goods_no_value[key])
+                    text = text.replace(r"\n", "\n")
+
+            # if key in ["跟高", "鞋宽", "帮高", "脚掌围", "鞋长"]:
+            #     if text:
+            #         text = text.split(".")[0]
+
+            if subsection_len != 0:
+                text = text.split("\n")
+                text = [x for x in text if x]
+                if len(text) == 2:
                     text_1 = text[0]
+                    text_2 = text[1]
+                    return text_1, text_2
                 else:
-                    text_1 = ""
-                text_2 = ""
-                return text_1, text_2
+                    if text:
+                        text_1 = text[0]
+                    else:
+                        text_1 = ""
+                    text_2 = ""
+                    return text_1, text_2
 
-        return text
+            return text
+        except:
+          raise UnicornException(f"缺少货号资料:[{key}],请检查系统商品信息或excel是否缺少该字段")
 
     def create_folder(self, path):
         if not os.path.exists(path):
+            print(f"创建目录   详情页--系统---=================>>>>:{path}")
             os.makedirs(path)
 
     def get_all_process_pics(self):
@@ -567,6 +569,7 @@ class DetailBase(object):
     def move_other_pic(self, move_main_pic=True):
         # ------------------------------移动其他图片------------------------------
         goods_no_main_pic_number = 0
+        sorted_list_800 = []
         for goods_art_no_dict in self.goods_no_value["货号资料"]:
             if "800x800" not in goods_art_no_dict:
                 continue
@@ -581,10 +584,12 @@ class DetailBase(object):
                 goods_art_no = goods_art_no_dict["货号"]
             # print("goods_art_no:", goods_art_no)
             # 移动颜色图=====================
-            goods_art_no_f = "{}/{}".format(self.out_put_dir, self.goods_no)
+            # goods_art_no_f = "{}/{}".format(self.out_put_dir, self.goods_no)
+            sorted_list_800 = natsorted(goods_art_no_dict["800x800"], key=lambda x: x.split("(")[1].split(")")[0])
+            goods_art_no_f = "{}".format(self.out_put_dir)
             self.create_folder(goods_art_no_f)
             # 放入一张主图
-            old_pic_path_1 = goods_art_no_dict["800x800"][0]
+            old_pic_path_1 = sorted_list_800[0]
             shutil.copy(
                 old_pic_path_1,
                 "{}/颜色图{}{}".format(
@@ -594,18 +599,25 @@ class DetailBase(object):
 
             # 把其他主图放入作为款号图=====================
             if move_main_pic:
-                for pic_path in goods_art_no_dict["800x800"]:
-                    goods_no_main_pic_number += 1
+                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}({goods_no_main_pic_number}){e}".format(
+                        "{out_put_dir}/主图{goods_no}({goods_no_main_pic_number}){suffix_name}{e}".format(
                             out_put_dir=self.out_put_dir,
-                            goods_no=self.goods_no,
+                            goods_no=goods_art_no,
                             goods_no_main_pic_number=str(
-                                goods_no_main_pic_number + 10
-                            ).zfill(2),
+                                index
+                            ),
                             e=e,
+                            suffix_name=suffix_name
                         ),
                     )
 
@@ -639,7 +651,7 @@ class DetailBase(object):
 
         for _index, main_pic_path_list in enumerate(all_main_pic_path_list):
             self.check_state_end()
-            out_path_root = "{out_put_dir}/{goods_no}/main_image_{_index}".format(
+            out_path_root = "{out_put_dir}/main_image_{_index}".format(
                 out_put_dir=self.out_put_dir, goods_no=self.goods_no, _index=_index
             )
             check_path(out_path_root)

+ 120 - 11
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
@@ -16,17 +17,20 @@ sqlite_url = f"sqlite:///{sqlite_file_name}"
 engine = create_engine(
     sqlite_url,
     echo=False,
-    connect_args={"check_same_thread": False},  # 允许多线程访问
-    pool_size=10,
-    max_overflow=20,
-    pool_timeout=30,
-    pool_recycle=1800,
+    connect_args={"check_same_thread": False},
+    pool_size=20,        # 增加基础连接池大小
+    max_overflow=30,     # 增加最大溢出连接数
+    pool_timeout=60,     # 保持合理的超时时间
+    pool_recycle=1800,   # 连接回收时间(秒)
+    pool_pre_ping=True,  # 检查连接有效性
 )
 
 
 # 创建表
 def create_all_database():
     SQLModel.metadata.create_all(engine)
+    # 执行自动迁移
+    auto_add_missing_columns()
 
 
 # 创建会话
@@ -138,10 +142,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_(
                 *(
@@ -150,11 +157,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):
@@ -180,6 +201,7 @@ class CRUD:
                 )
             )
         )
+        print("SQL 打印==>",str(query))
         result = session.exec(query).first()
         if result:
             for key, value in kwargs.items():
@@ -188,6 +210,32 @@ class CRUD:
             return result
         return None
 
+    def updateConditionsAll(self, session: Session, conditions: Dict, **kwargs):
+        """
+        根据条件更新记录
+        :param session: 数据库会话
+        :param conditions: 更新条件字典
+        :param kwargs: 需要更新的字段和值
+        :return: 更新后的对象
+        """
+        query = select(self.model).where(
+            and_(
+                *(
+                    getattr(self.model, key) == value
+                    for key, value in conditions.items()
+                )
+            )
+        )
+        print("SQL 打印==>", str(query))
+        results = session.exec(query).fetchall()
+        if results:
+            for obj in results:  # 遍历每个对象
+                for key, value in kwargs.items():
+                    setattr(obj, key, value)  # 对每个对象设置属性
+            session.commit()  # 提交事务以保存更改
+            return results
+        return None
+
 
 # 批量插入数据到设备配置表
 def batch_insert_device_configs(session: Session, action_tabs: list, data_list: list):
@@ -258,9 +306,70 @@ 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
 
 
+def auto_add_missing_columns():
+
+    """
+    自动检测并添加缺失的数据库字段(最简化版本)
+    只为 device_config 表添加缺失的字段
+    """
+    try:
+        import sqlite3
+        
+        # 连接数据库
+        conn = sqlite3.connect(sqlite_file_name)
+        cursor = conn.cursor()
+        
+        # 检查表是否存在
+        cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='device_config'")
+        if not cursor.fetchone():
+            conn.close()
+            return
+        
+        # 获取现有字段
+        cursor.execute("PRAGMA table_info(device_config)")
+        existing_columns = [row[1] for row in cursor.fetchall()]
+        
+        # 定义需要添加的字段
+        # 格式: (字段名, SQL类型, 默认值)
+        # 根据你在 DeviceConfig model 中新增的字段来配置
+        new_fields = [
+            # 请在这里添加你实际新增的字段
+            ("point_name", "VARCHAR", "DEFAULT 'A'"),
+            ("is_move_device", "BOOLEAN", "DEFAULT 1")
+        ]
+        
+        # 添加缺失的字段
+        for field_name, field_type, default_clause in new_fields:
+            if field_name not in existing_columns:
+                try:
+                    sql = f"ALTER TABLE device_config ADD COLUMN {field_name} {field_type} {default_clause}"
+                    cursor.execute(sql)
+                    conn.commit()
+                except Exception as e:
+                    print(f"⚠️ 添加字段失败: {e}")
+                    pass  # 忽略错误,继续下一个
+        
+        conn.close()
+        
+    except Exception as e:
+        print(f"⚠️ 添加字段失败: {e}")
+        pass  # 静默失败,不影响启动
+
+
+
+
 def SqlQuery():
     return next(__get_session())
 

+ 43 - 23
python/detail_template_test.json

@@ -3,43 +3,63 @@
         "款号": "AQN191159",
         "货号资料": [
             {
-                "货号": "AQN1911592",
-                "文件夹名称": "AQN1911592",
+                "货号": "AC57000082",
+                "文件夹名称": "AC57000082",
                 "编号": "",
                 "颜色名称": "棕色",
                 "pics": {
-                    "俯视-抠图": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(1)_俯视_抠图.png",
-                    "俯视-阴影": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(1)_俯视_阴影.png",
-                    "侧视-抠图": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(2)_侧视_抠图.png",
-                    "侧视-阴影": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(2)_侧视_阴影.png",
-                    "后跟-抠图": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(3)_后跟_抠图.png",
-                    "后跟-阴影": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(3)_后跟_阴影.png",
-                    "鞋底-抠图": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(4)_鞋底_抠图.png",
-                    "鞋底-阴影": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(4)_鞋底_阴影.png",
-                    "内里-抠图": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(5)_内里_抠图.png",
-                    "内里-阴影": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(5)_内里_阴影.png",
-                    "其他1-抠图": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(6)_其他1_抠图.png",
-                    "其他1-阴影": "output/2025-09-01/AQN1911592/阴影图处理/AQN1911592(6)_其他1_阴影.png"
+                    "俯视-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(1)_俯视_抠图.png",
+                    "俯视-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(1)_俯视_阴影.png",
+                    "侧视-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(2)_侧视_抠图.png",
+                    "侧视-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(2)_侧视_阴影.png",
+                    "后跟-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(3)_后跟_抠图.png",
+                    "后跟-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(3)_后跟_阴影.png",
+                    "鞋底-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(4)_鞋底_抠图.png",
+                    "鞋底-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(4)_鞋底_阴影.png",
+                    "内里-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(5)_内里_抠图.png",
+                    "内里-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(5)_内里_阴影.png"
                 },
                 "800x800": [
-                    "output/2025-09-01/AQN1911592/800x800/AQN1911592(1)_1600.png",
-                    "output/2025-09-01/AQN1911592/800x800/AQN1911592(2)_1600.png",
-                    "output/2025-09-01/AQN1911592/800x800/AQN1911592(3)_1600.png",
-                    "output/2025-09-01/AQN1911592/800x800/AQN1911592(4)_1600.png",
-                    "output/2025-09-01/AQN1911592/800x800/AQN1911592(5)_1600.png",
-                    "output/2025-09-01/AQN1911592/800x800/AQN1911592(6)_1600.png"
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(1)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(2)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(3)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(4)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(5)_320.png"
+                ]
+            },
+            {
+                "货号": "AC57000083",
+                "文件夹名称": "AC57000083",
+                "编号": "",
+                "颜色名称": "棕色",
+                "pics": {
+                    "俯视-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(1)_俯视_抠图.png",
+                    "俯视-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(1)_俯视_阴影.png",
+                    "侧视-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(2)_侧视_抠图.png",
+                    "侧视-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(2)_侧视_阴影.png",
+                    "后跟-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(3)_后跟_抠图.png",
+                    "后跟-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(3)_后跟_阴影.png",
+                    "鞋底-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(4)_鞋底_抠图.png",
+                    "鞋底-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(4)_鞋底_阴影.png",
+                    "内里-抠图": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(5)_内里_抠图.png",
+                    "内里-阴影": "output/2025-10-23/AC57000082/阴影图处理/AC57000082(5)_内里_阴影.png"
+                },
+                "800x800": [
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(1)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(2)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(3)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(4)_320.png",
+                    "output/2025-10-23/AC57000082/800x800/AC57000082(5)_320.png"
                 ]
             }
         ],
         "商品面料": "头层牛皮",
         "商品内里": "猪皮革",
-        "商品鞋底": "橡胶",
         "鞋垫": "猪皮革",
         "商品标题": "舒适休闲男豆豆鞋男单鞋",
         "商品价格": "132.00",
         "性别": "男性",
         "token": "Bearer 18323b96c68234597b1fa8d10fecb6bbe45cadc3",
-        "场景图": "output/2025-09-01/AQN1911592/场景 图.jpg",
-        "模特图": "output/2025-09-01/AQN1911592/模特图.jpg"
+         "场景图": "output/2025-10-23/AC57000082/场景图.jpg"
     }
 }

+ 4 - 5
python/detail_template_test.py

@@ -4,18 +4,17 @@ import settings
 settings.is_test_plugins = True
 
 
-from custom_plugins.plugins.detail_template.huilima.detail_huilima1 import (
+from custom_plugins.plugins.detail_template.qingyangyizhan.detail_qingyangyizhan2 import (
     DetailPicGet,
 )
 
 
-data = json.load(open("detail_template_test.json", mode="r", encoding="utf-8"))
+data = json.load(open("detail_template_test_qingyangyizhan.json", mode="r", encoding="utf-8"))
 for goods_no, value in data.items():
-    print("value", value)
     d = DetailPicGet(
         goods_no=goods_no,
         goods_no_value=value,
-        out_put_dir=r"C:\Development\project\python\CameraMachine\python\output\qingyangyizhan-1",
-        test=True,
+        out_put_dir=r"C:\Development\project\python\CameraMachine\python\output\detail_qingyangyizhan2",
+        test=False,
     )
 print("生成成功")

+ 12 - 21
python/detail_template_test_qingyangyizhan.json

@@ -8,29 +8,20 @@
                 "编号": "AC51016112",
                 "颜色名称": "枪色",
                 "pics": {
-                    "俯视-抠图": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(1)_俯视_抠图.png",
-                    "俯视-阴影": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(1)_俯视_阴影.png",
-                    "侧视-抠图": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(2)_侧视_抠图.png",
-                    "侧视-阴影": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(2)_侧视_阴影.png",
-                    "后跟-抠图": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(3)_后跟_抠图.png",
-                    "后跟-阴影": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(3)_后跟_阴影.png",
-                    "鞋底-抠图": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(4)_鞋底_抠图.png",
-                    "鞋底-阴影": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(4)_鞋底_阴影.png",
-                    "内里-抠图": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(5)_内里_抠图.png",
-                    "内里-阴影": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(5)_内里_阴影.png",
-                    "组合-抠图": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(6)_其他1_抠图.png",
-                    "组合-阴影": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(6)_其他1_阴影.png",
-                    "组合2-抠图": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(7)_其他0_抠图.png",
-                    "组合2-阴影": "C:/Users/15001/Desktop/ningbogongmao/阴影图处理/A12345(7)_其他0_阴影.png"
+                    "俯视-抠图": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(1)_俯视_抠图.png",
+                    "俯视-阴影": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(1)_俯视_阴影.png",
+                    "侧视-抠图": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(2)_侧视_抠图.png",
+                    "侧视-阴影": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(2)_侧视_阴影.png",
+                    "后跟-抠图": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(3)_后跟_抠图.png",
+                    "后跟-阴影": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(3)_后跟_阴影.png",
+                    "鞋底-抠图": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(4)_鞋底_抠图.png",
+                    "鞋底-阴影": "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/阴影图处理/A596742(4)_鞋底_阴影.png"
                 },
                 "800x800": [
-                    "C:/Users/15001/Desktop/ningbogongmao/800x800/A12345(1)_1600.jpg",
-                    "C:/Users/15001/Desktop/ningbogongmao/800x800/A12345(2)_1600.jpg",
-                    "C:/Users/15001/Desktop/ningbogongmao/800x800/A12345(3)_1600.jpg",
-                    "C:/Users/15001/Desktop/ningbogongmao/800x800/A12345(4)_1600.jpg",
-                    "C:/Users/15001/Desktop/ningbogongmao/800x800/A12345(5)_1600.jpg",
-                    "C:/Users/15001/Desktop/ningbogongmao/800x800/A12345(6)_1600.jpg",
-                    "C:/Users/15001/Desktop/ningbogongmao/800x800/A12345(7)_1600.jpg"
+                    "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/800x800/A596742(1)_1600.png",
+                    "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/800x800/A596742(2)_1600.png",
+                    "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/800x800/A596742(3)_1600.png",
+                    "C:/Users/15001/Desktop/宁波工贸-卷发棒/A596742/800x800/A596742(4)_1600.png"
                 ]
             }
         ],

+ 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"
+}
+```
 ##### 未完待续.....

+ 19 - 7
python/mcu/BaseClass.py

@@ -1,4 +1,4 @@
-import asyncio
+import asyncio,time
 from sockets import ConnectionManager
 from utils.common import message_queue
 from mcu.capture.smart_shooter_class import SmartShooter
@@ -17,21 +17,33 @@ class BaseClass:
         # self.device_status = 2
 
     def sendSocketMessage(self, code=0, msg="", data=None, device_status=2):
-        data = {
+        t_start = time.time()
+        payload = {
             "code": code,
             "msg": msg,
             "status": device_status,
             "data": data,
             "msg_type": self.msg_type,
         }
+        
+        print(f"[T1: {t_start:.4f}] sendSocketMessage 调用, msg={msg}")
+
         loop = asyncio.get_event_loop()
         if self.websocket == None:
-            loop.create_task(message_queue.put(data))
+            print(f"[T1: {time.time()-t_start:.4f}s] 走队列路径")
+            loop.create_task(message_queue.put(payload))
         else:
-            loop.create_task(
-                self.websocket_manager.send_personal_message(data, self.websocket)
-            )
-
+            print(f"[T1: {time.time()-t_start:.4f}s] 走直接发送路径")
+            
+            async def _do_send():
+                t2 = time.time()
+                print(f"[T2: {t2-t_start:.4f}s] 任务开始执行, 准备调用 send_personal_message")
+                await self.websocket_manager.send_personal_message(payload, self.websocket)
+                t3 = time.time()
+                print(f"[T3: {t3-t_start:.4f}s] send_personal_message 完成, 总耗时: {t3-t2:.4f}s")
+                
+            loop.create_task(_do_send())
+        print("\033[1;32;40m 发送消息===>sendSocketMessage \033[0m", data)
     async def asyncSendSocketMessage(self, code=0, msg="", data=None, device_status=2):
         data = {
             "code": code,

+ 3 - 3
python/mcu/BlueToothMode.py

@@ -64,9 +64,9 @@ class BlueToothMode(BaseClass, metaclass=SingletonType):
             devices = await BleakScanner.discover()
         except BaseException as e:
             self.print_error("蓝牙疑似未打开,{}".format(e))
-            self.sendSocketMessage(
-                code=0, msg="蓝牙疑似未打开,{}".format(e), data=None, device_status=-1
-            )
+            # self.sendSocketMessage(
+            #     code=0, msg="蓝牙疑似未打开,{}".format(e), data=None, device_status=-1
+            # )
             return
         for d in devices:
             # print(f"Device: {d.name} - Address: {d.address}")

+ 318 - 108
python/mcu/DeviceControl.py

@@ -5,6 +5,7 @@ import serial.tools.list_ports
 import time, json
 from .SerialIns import SerialIns
 from utils.SingletonType import SingletonType
+from utils.utils_func import dynamic_parameter_issuance_get
 from .BaseClass import BaseClass
 from sockets import ConnectionManager
 from collections import defaultdict
@@ -20,7 +21,8 @@ from .LineControl import LineControl
 import copy
 import logging
 from mcu.capture.smart_shooter_class import SmartShooter
-
+import logging
+from conifg_info import ConfigManager
 logger = logging.getLogger(__name__)
 
 
@@ -34,7 +36,26 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         super().__init__(
             websocket_manager=websocket_manager, smart_shooter=smart_shooter
         )
+        self.camera_height = 400
+        self.config_manager = None
         self.msg_type = "mcu"
+        self.command = {
+            "to_device_move": 1,  # 设备运动
+            "to_init_device": 2,  # 初始化设备
+            "to_deal_other_device": 3,  # 处理其他设备
+            "get_all_info": 29,  # 获取所有信息
+            "set_deviation": 40,  # 设置偏移量
+            "get_deviation": 41,  # 读取偏移量
+            "signal_forwarding": 91,  # 信号转发处理
+            "signal_forwarding_return": 92,  # 信号转发返回
+            "get_other_info": 44,  # 获取其他信息
+            "open_rgb_led": 43,  ## RGB灯的处理与通讯
+            "set_other_info": 45,  # 设置其他信息
+            "query_remote_control_battery": 47,  # 查询遥控器电量
+            "set_turntable_mode": 48,  # 设置转盘通讯方式 1、串口、2、无线、3 混合
+            "stop_mcu": 93,  # 停止运行mcu
+        }
+        self.serial_ins = None
         self.mcu_deviation_set = McuDeviationSet(self)
         self.mcu_other_set = OtherSet(self)
         self.debug_uart = DebugUart(self)
@@ -47,6 +68,10 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         self.state_camera_steering = 3
         self.state_turntable_steering = 3
         self.state_overturn_steering = 3
+        self.seed = 0
+        self.last_camera_height = 0
+        # 是否实时获取mcu状态信息
+        self.is_get_mcu_state = True
         self.state_move_turntable_steering = 3
         self.last_from_mcu_move_respond_data = None
         self.camera_motor_speed = 0
@@ -55,7 +80,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         self.port_name = ""
         self.mcu_exit = False
         self.t_n = 0
-        self.serial_ins = None
+
         self.connected_ports_dict = {}  # 已连接的ports
         self.p_list = []
         self.temp_ports_dict = {}
@@ -76,7 +101,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,
@@ -90,22 +127,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             "data": {},
         }
 
-        self.command = {
-            "to_device_move": 1,  # 设备运动
-            "to_init_device": 2,  # 初始化设备
-            "to_deal_other_device": 3,  # 处理其他设备
-            "get_all_info": 29,  # 获取所有信息
-            "set_deviation": 40,  # 设置偏移量
-            "get_deviation": 41,  # 读取偏移量
-            "signal_forwarding": 91,  # 信号转发处理
-            "signal_forwarding_return": 92,  # 信号转发返回
-            "get_other_info": 44,  # 获取其他信息
-            "open_rgb_led": 43,  ## RGB灯的处理与通讯
-            "set_other_info": 45,  # 设置其他信息
-            "query_remote_control_battery": 47,  # 查询遥控器电量
-            "set_turntable_mode": 48,  # 设置转盘通讯方式 1、串口、2、无线、3 混合
-            "stop_mcu": 93,  # 停止运行mcu
-        }
+
         # self.window = window
         self.last_push_time = defaultdict(float)
         self.is_running = False
@@ -125,7 +147,119 @@ 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,  # 打印回执
+            124: self.read_register_data_by_usb,  # 读取某个寄存器数据
+            125: self.write_register_data_by_usb,  # 写入某个寄存器数据
+            126: self.get_all_registers_list_by_usb,  # 获取所有寄存器
+            150: self.dynamic_parameter_issuance,  # 动态参数下发
+        }
+    def get_device_info(self):
+        if not self.init_state:
+            self.sendSocketMessage(code=1, msg="mcu设备未初始化", device_status=4)
+            return False
+        cmd = [124]
+        cmd.extend([0xff & 106 >> 8, 0xff & 106])
+        # print("get_device_info cmd",cmd)
+        data = self.get_basic_info_mcu_without_async(data=cmd)
+        # print("get_device_info====>",data)
+        if not data:
+            return False
+        return_data = self.analysis_data(data[1:])
+        if return_data:
+            camera_height = return_data.get('value',35)
+            self.camera_height = camera_height
+        else:
+            self.camera_height = 400
+        self.msg_type = 'get_device_info'
+        self.sendSocketMessage(
+            code=0,
+            msg="设置mcu其他配置信息完成",
+            device_status=2,
+            data={"camera_height":self.camera_height}
+        )
+        self.msg_type = 'mcu'
+        return self.camera_height
+    # 获取异步数据
+    def analysis_data(self, _data):
+        _addr = _data[0] << 8 | _data[1]
+        if _addr not in self.config_manager.CONFIG_METADATA_BY_ADDR:
+            return False
+        start = 2
+        _value = _data[start] << 40 | _data[start + 1] << 32 | _data[start + 2] << 24 | _data[start + 3] << 16 | _data[start + 4] << 8 | _data[start + 5]
+
+        start = start + 5
+        _read_only = True if _data[start + 1] == 1 else False  # 是否只读
+        _dir = 1 if _data[start + 2] == 1 else -1
+        _type = "int" if _data[start + 3] == 1 else "float"
+        _precision = _data[start + 4]
+
+        if _dir < 0:
+            _value = _value * _dir
+
+        if _type == "float":
+            if _precision > 0:
+                _round_x = _precision
+                _precision = _precision * -1
+                _value = _value * 10 ** _precision
+                _value = round(_value, _round_x)
+        else:
+            _value = int(_value)
+
+        return_data = {"addr": _addr,
+                       "key_name": self.config_manager.CONFIG_METADATA_BY_ADDR[_addr],
+                       "readonly": _read_only,
+                       "value": _value}
+        return return_data
+        # 跳过异步,直接查询某个数据信息
+    def get_basic_info_mcu_without_async(self, data,fiddler_cmd=0):
+        """
+        fiddler_cmd :只接收指定的命令内容
+        """
+        try:
+            # 清空接收数据
+            self.serial_ins.clearn_flush()
+            self.serial_ins.write_cmd(data)
+            time.sleep(0.06)
+            r_data = self.get_basic_info_mcu()
+            print("264----------r_data:", r_data)
+        except BaseException as e:
+            print("302---e",e)
+            r_data = []
+        #
+        # self.async_lock.release()
+        return r_data
+    def dynamic_parameter_issuance(self, receive_data):
+        print("dynamic_parameter_issuance   receive_data", receive_data)
+        func_code, status_code, out_par_data_list = dynamic_parameter_issuance_get(
+            receive_data
+        )
+        r_data = {
+            "func_code": func_code,
+            "status_code": status_code,
+            "out_par_data_list": out_par_data_list,
         }
+        print(r_data)
+        return r_data
+
+    # 读取某个寄存器数据
+    def read_register_data_by_usb(self, receive_data):
+        return receive_data
+
+    # 写入某个寄存器数据
+    def write_register_data_by_usb(self, receive_data):
+        return receive_data
+
+    # 获取所有寄存器
+    def get_all_registers_list_by_usb(self, receive_data):
+        byte_list = receive_data[1:]
+        _r_data = []
+        for i in range(0, len(byte_list), 2):
+            if i + 1 < len(byte_list):
+                first_byte = byte_list[i]
+                second_byte = byte_list[i + 1]
+                _addr = first_byte << 8 | second_byte
+                _r_data.append(_addr)
+        return _r_data
 
     async def sendCommand(self, command):
         await asyncio.sleep(0.01)
@@ -188,30 +322,22 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             logger.info("已经初始化过,请勿重复初始化")
             self.sendSocketMessage(msg="设备初始化完成", device_status=2)
             return False
+        self.config_manager = ConfigManager()
         self.serial_ins.clearn_flush()
         self.to_init_device_origin_point(device_name="mcu", is_force=is_force)
         print("MCU 开始循环~")
         logger.info("MCU 开始循环~")
         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")
                 self.send_cmd()
-                # time.sleep(0.01)
                 if not self.get_basic_info_mcu():
                     pass
-                # self.close_other_window()
             except BaseException as e:
                 print("121231298908", e)
-                logger.info("121231298908", e)
+                logger.info(f"121231298908{e}")
                 break
 
         self.is_running = False
@@ -411,47 +537,95 @@ 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)
+                # 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)
+                # self.sendSocketMessage(msg="命令发送完成", device_status=2)
             else:
                 break
 
     def send_cmd(self):
         self.lock.acquire()
         if self.send_data_queue:
-            self.sendSocketMessage(msg="正在发送命令", device_status=1)
+            # self.sendSocketMessage(msg="正在发送命令", device_status=1)
             data = self.send_data_queue.pop(0)
             self.serial_ins.write_cmd(data)
-            self.sendSocketMessage(msg="命令发送完成", device_status=2)
-        # else:
-        #     self.t_n += 1
-        #     # 加大发送获取基础数据的时间间隔
-        #     # 默认为0.01秒一个循环,每隔1.5秒发送数据
-        #     if self.t_n > 150:
-        #         self.t_n = 0
-        #         data = [self.command["get_all_info"], 1]
-        #         self.serial_ins.write_cmd(data)
+            # self.sendSocketMessage(msg="命令发送完成", device_status=2)
+        else:
+            # self.t_n += 1
+            # 加大发送获取基础数据的时间间隔
+            # 默认为0.01秒一个循环,每隔1.5秒发送数据
+            if self.t_n == 150:
+                # self.t_n = 0
+                self.send_get_all_info_to_mcu()
         self.lock.release()
 
+    def send_get_all_info_to_mcu(self):
+        if self.is_get_mcu_state is False:
+            return
+        data = [self.command["get_all_info"], 1]
+        self.serial_ins.write_cmd(data)
+
     def print_mcu_error_data(self, receive_data):
         # 扫码数据
         try:
             data = receive_data[1:].decode()
             if "设备初始化完成" in data:
                 self.init_state = True
+                logger.info(f"设备初始化完成:%{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(f"115  设备异常数据打印:%{data}")
         except BaseException as e:
             print("117 error {}".format(e))
-            logger.info("117 error %s", e)
+            logger.info(f"117 error %{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(f"设备运动消息回执:{message_info}")
+            print("接收设备消息回执:", command_text)
+            logger.info(f"接收设备消息回执:{receive_data_temp_text}")
+        except BaseException as e:
+            print(f"255 error {e}")
+            logger.info(f"255 error {e}")
         return
-
     def get_from_mcu_move_respond_data(self, receive_data):
         self.last_from_mcu_move_respond_data = receive_data
 
@@ -465,7 +639,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
 
         # self.self_sign.emit({"type": "connect_sign", "data": connect_flag})
         message = {"type": "connect_sign", "data": connect_flag}
-        self.sendSocketMessage(msg="接收接信息", data=message)
+        self.sendSocketMessage(msg="接收接信息", data=message)
         print("接收链接信息")
         logger.info("接收链接信息")
         return
@@ -539,21 +713,20 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         receive_data = self.serial_ins.read_cmd(out_time=1)
         if receive_data is False:
             print("------------------------------------------------4657564654")
+            print(
+                "------------------------------------------------get_basic_info_mcu------------------"
+            )
             logger.info("------------------------------------------------4657564654")
             self.connect_state = False
             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)
+            _data = ' '.join([hex(x) for x in receive_data])
+            return self.deal_code_func_dict[command](receive_data)
+        return False
 
     def get_from_mcu_button(self, receive_data):
         button_name = receive_data[1]
@@ -851,7 +1024,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:
@@ -895,7 +1070,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:
@@ -916,7 +1093,8 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         ports = serial.tools.list_ports.comports()
         # 遍历所有端口并打印信息
         for port in ports:
-            if "CH340" in port.description:
+            # logger.info("扫描到的串口信息:{}".format(port.description))
+            if "CH34" in port.description:
                 ports_dict[port.name] = {
                     "name": port.name,
                     "device": port.device,
@@ -1029,8 +1207,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:
@@ -1074,8 +1252,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
@@ -1224,35 +1402,41 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             self.init_state = False
             print("关闭MCU")
             logger.info("关闭MCU")
+
     def close_lineConnect(self):
-        ''''关闭有线遥控器连接'''
+        """'关闭有线遥控器连接"""
         self.line_control.port_name = ""
         self.line_control.close_connect()
         print("关闭有线遥控器")
         logger.info("关闭有线遥控器")
+
     @property
     def mcu_move_state(self):
-        if self.m_t == 1:
-            if (
-                self.state_camera_motor == 2
-                and self.state_camera_steering == 2
-                and self.state_turntable_steering == 2
-                and self.state_overturn_steering == 2
-            ):
-                self._mcu_move_state = 2
-            else:
-                self._mcu_move_state = 1
+        if self.is_get_mcu_state is False:
+            self._mcu_move_state = 2
+            # self.action_state = 2
         else:
-            if (
-                self.state_camera_motor == 2
-                and self.state_camera_steering == 2
-                and self.state_turntable_steering == 2
-                and self.state_overturn_steering == 2
-                and self.state_move_turntable_steering == 2
-            ):
-                self._mcu_move_state = 2
+            if self.m_t == 1:
+                if (
+                    self.state_camera_motor == 2
+                    and self.state_camera_steering == 2
+                    and self.state_turntable_steering == 2
+                    and self.state_overturn_steering == 2
+                ):
+                    self._mcu_move_state = 2
+                else:
+                    self._mcu_move_state = 1
             else:
-                self._mcu_move_state = 1
+                if (
+                    self.state_camera_motor == 2
+                    and self.state_camera_steering == 2
+                    and self.state_turntable_steering == 2
+                    and self.state_overturn_steering == 2
+                    and self.state_move_turntable_steering == 2
+                ):
+                    self._mcu_move_state = 2
+                else:
+                    self._mcu_move_state = 1
 
         # self._mcu_move_state = 2
         return self._mcu_move_state
@@ -1292,14 +1476,17 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         _is_debug=0,
         is_relative=0,
         is_deviation=1,
+        times=2, is_response=False
     ):
         """
         此处输入单位为 毫米,以及度  需要先缩小,再放大
         """
+        self.seed += 1
+        if self.seed > 9000:
+            self.seed = 1
         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:
@@ -1325,9 +1512,12 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                     if down_speed is None
                     else down_speed
                 )
-
                 value = value / 10  # value 单位毫米
-                assert 0 <= value <= 40
+                max_camera_hight = self.camera_height/10
+                # print("高度位置",max_camera_hight)
+                if value > max_camera_hight:
+                    value = max_camera_hight
+                assert 0 <= value <= max_camera_hight
                 assert 0 <= max_speed <= 10000
             case "camera_steering":
                 # 角度为度 未放大 精确到0.1度
@@ -1390,14 +1580,20 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             _is_debug,
             is_deviation,
             is_relative,
+            times,  # 重试次数
+            0xff & self.seed >> 8,  # 随机种子
+            0xff & self.seed,  # 随机种子
+            1 if is_response is True else 0,  # 是否返回内容
         ]
         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:
             self.lock.acquire()
             # print('==========================>1111')
-            # print("-------------------to_get_mcu_base_info--------------------------")
+            print("-------------------to_get_mcu_base_info--------------------------")
             data = [self.command["get_all_info"], 1]
             f = True
             try:
@@ -1449,11 +1645,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)
@@ -1461,27 +1656,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":
                 # 翻板舵机中位
@@ -1506,8 +1704,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("检查设备是否运行中")
@@ -1546,6 +1742,8 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                     new_init_config["shoe_upturn"] = False
                     new_init_config["pre_delay"] = 0.0
                     new_init_config["after_delay"] = 0.0
+                    # new_init_config["point_name"] = "A"
+                    new_init_config["is_move_device"] = True
                     new_init_config["led_switch"] = True
                     new_init_config["turntable_angle"] = 0.0
                     new_init_config["is_wait"] = False
@@ -1583,6 +1781,9 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 },
             )
             await self.controlDevice("laser_position", 0)
+            # await smart_shooter.EnableCameraPreview(
+            #         enable_status=True, msg_type="smart_shooter_enable_preview"
+            #     )
             self.msg_type = "mcu"
             self.is_runn_action = True
             for index, action in enumerate(config_list):
@@ -1606,7 +1807,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异常终止")
@@ -1653,7 +1856,10 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             )
             self.msg_type = "mcu"
             await self.controlDevice("laser_position", 1)
-
+        self.action_state = 2
+        # await smart_shooter.EnableCameraPreview(
+        #             enable_status=False, msg_type="smart_shooter_enable_preview"
+        #         )
     async def run_mcu_config_single(
         self,
         config_info,
@@ -1680,17 +1886,20 @@ 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异常终止")
                 logger.info("action异常终止")
                 return
-            self.msg_type = "run_mcu_single"
             program_item.smart_shooter = smart_shooter
             await program_item.run(3)
             self.msg_type = "mcu"
-            print("发送 run_mcu_signle消息","{} 执行完成~".format(program_item.action_name))
+            logger.info(
+                "发送 run_mcu_signle消息,{} 执行完成~".format(program_item.action_name)
+            )
             self.sendSocketMessage(
                 code=0,
                 msg="{} 执行完成~".format(program_item.action_name),
@@ -1698,8 +1907,8 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 device_status=2,
             )
             self.action_state = 2
-            self.msg_type = msg_type
-            print("发送 run_mcu_signle消息", "执行完成")
+            self.msg_type = "run_mcu_single"
+            logger.info("发送 run_mcu_signle消息执行完成")
             self.sendSocketMessage(
                 code=0,
                 msg=f"执行完成",
@@ -1710,12 +1919,13 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
             self.sendSocketMessage(
                 code=1, msg="未查询到重拍记录得配置信息,请确认", device_status=0
             )
+        self.action_state = 2
 
     async def only_take_photo(self, goods_art_no, image_index, record_id):
         await asyncio.sleep(0.1)
         print("only_take_photo=====>", goods_art_no, image_index, record_id)
         logger.info(
-            "only_take_photo=====> %s %s %s", goods_art_no, image_index, record_id
+            f"only_take_photo=====> {goods_art_no} {image_index} {record_id}"
         )
         if goods_art_no == "":
             print("only_take_photo 数据查询异常")
@@ -1761,7 +1971,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:

+ 65 - 2
python/mcu/LineControl.py

@@ -9,7 +9,9 @@ from databases import SqlQuery, PhotoRecord, DeviceConfig, CRUD, insert_photo_re
 from .capture.module_digicam import DigiCam
 from .capture.module_watch_dog import FileEventHandler
 from sockets.connect_manager import ConnectionManager
-
+import settings,os
+import logging
+logger = logging.getLogger(__name__)
 class LineControl(BaseClass):
     # sign_data = Signal(dict)
 
@@ -181,6 +183,7 @@ class LineControl(BaseClass):
             message = {"_type": 0, "plugins_mode": "remote_control", "data": bar_code}
             print("有线控制器 扫码数据1", message)
             self.goods_art_no = bar_code
+            self.saveScanData(self.goods_art_no)
             self.sendSocketMessage(code=0, msg="", data=message, device_status=2)
             return
         if receive_data[0] == 9:
@@ -193,6 +196,7 @@ class LineControl(BaseClass):
                     self.sendSocketMessage(1, "前置拍照未完成,请稍后", device_status=-1)
                     return
                 print("收到货号信息", self.goods_art_no)
+                # self.saveScanData(self.goods_art_no)
                 self.handlerAction(button_value)
                 self.photo_take_state = 0
             if button_value in [3]:
@@ -214,7 +218,66 @@ class LineControl(BaseClass):
             self.sendSocketMessage(code=0, msg="", data=message, device_status=2)
             return
         pass
-
+    def saveScanData(self, scan_data):
+        """保存扫码数据"""
+        if scan_data == None or scan_data == "":
+            return
+        scan_path = settings.SCAN_DIR
+        if scan_path ==None or scan_path == "":
+            return
+        try:
+            # 验证文件名的有效性
+            # 替换可能导致问题的字符
+            invalid_chars = '<>:"/\\|?*'
+            safe_scan_data = scan_data
+            for char in invalid_chars:
+                safe_scan_data = safe_scan_data.replace(char, '_')
+            # 确保文件名不为空
+            if not safe_scan_data.strip():
+                logger.info(f"扫码文件,无效的文件名")
+                return
+            
+            # 获取目录下所有txt文件
+            txt_files = [f for f in os.listdir(scan_path) if f.lower().endswith('.txt')]
+            txt_filename = safe_scan_data + ".txt"
+            new_txt_path = os.path.join(scan_path, txt_filename)
+            # 如果只有一个txt文件,则重命名并覆盖内容
+            if len(txt_files) == 1:
+                try:
+                    old_txt_path = os.path.join(scan_path, txt_files[0])
+                    os.rename(old_txt_path, new_txt_path)
+                    
+                    # 覆盖文件内容
+                    with open(new_txt_path, 'w', encoding='utf-8') as f:
+                        f.write(scan_data)
+                        
+                except Exception as e:
+                    logger.info(f"扫码文件,重命名或写入文件失败: {e}")
+                    
+                    # 如果重命名失败,创建新文件
+                    with open(new_txt_path, 'w', encoding='utf-8') as f:
+                        f.write(scan_data)
+            
+            # 如果有多个txt文件,删除它们并创建新文件
+            elif len(txt_files) > 1:
+                for txt_file in txt_files:
+                    try:
+                        txt_file_path = os.path.join(scan_path, txt_file)
+                        os.remove(txt_file_path)
+                    except Exception as e:
+                        logger.info(f"扫码文件,删除文件失败 {txt_file}: {e}")
+                
+                # 创建新的txt文件
+                with open(new_txt_path, 'w', encoding='utf-8') as f:
+                    f.write(scan_data)
+            
+            # 如果没有txt文件,直接创建新文件
+            else:
+                with open(new_txt_path, 'w', encoding='utf-8') as f:
+                    f.write(scan_data)
+                
+        except Exception as e:
+            logger.info(f"扫码文件,保存文件时发生错误: {e}")
     async def run(self):
         self.is_running = True
         while True:

+ 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:

+ 176 - 59
python/mcu/ProgramItem.py

@@ -1,13 +1,16 @@
 import asyncio
 import json
 import os
+import traceback
 
 from .BaseClass import BaseClass
 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 +25,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表示没有等待
@@ -48,9 +52,12 @@ class ProgramItem(BaseClass):
         # self.action_type = self.get_value(action_data, "execution_type", "程序1")
         self.action_name = self.get_value(action_data, "action_name", "")
         self.is_wait = self.get_value(action_data, "is_wait", False)
+        self.point_name = self.get_value(action_data, "point_name", "A")
+        self.is_move_device = self.get_value(action_data, "is_move_device", True)
         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 +65,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 +79,7 @@ class ProgramItem(BaseClass):
 
         self.set_other()
         self.error_info_text = ""  # 错误提示信息
-
+        self.last_move_time = None
         # self.setParent(parent)
         self.mcu = mcu
 
@@ -150,8 +158,11 @@ class ProgramItem(BaseClass):
         last_num_1 = self.mcu.last_mcu_info_data["num"]
         await self.mcu.cleanAllReceiveData()
         while 1:
+            print("\033[1;31m执行检查动作\033[0m", self.mcu.mcu_move_state)
+            print("\033[1;31m执行检查动作\033[0m", self.mcu.action_state)
             if self.mcu.action_state != 1:
                 # 外部终止,停止运行
+                print("\033[1;31m执行结束\033[0m", "外部终止,停止运行")
                 return False
             cr_time = time.time()
             print(cr_time - _s, cr_time, _s)
@@ -160,21 +171,55 @@ class ProgramItem(BaseClass):
                 self.set_state(state_value=99)  # 标记异常
                 print("MCU检测运动未停止,自动退出")
                 self.sendSocketMessage(msg=self.error_info_text, device_status=-1)
+                print("\033[1;31m执行结束\033[0m", "MCU检测运动未停止,自动退出")
                 return False
                 # return True
             # 存在时间间隙,导致误认为所有设备已完成运动
             if self.mcu.mcu_move_state == 2:
+                print("\033[1;31m执行结束\033[0m", "导致误认为所有设备已完成运动")
                 return True
             else:
                 self.mcu.to_get_mcu_base_info()
-                await self.mcu.send_all_cmd()
+                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
-
-            await asyncio.sleep(0.1)
-            # self.mcu.to_get_mcu_base_info()
-
+        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 = "其他配置"
@@ -188,9 +233,11 @@ class ProgramItem(BaseClass):
             try:
                 await self.do_run()
             except BaseException as e:
-                self.sendSocketMessage(
-                    msg="p_item 错误:{}".format(e), device_status=-1
-                )
+                traceback.print_exc()
+                logger.info(f"p_item 错误 {e}")
+                # self.sendSocketMessage(
+                #     msg="p_item 错误:{}".format(e), device_status=-1
+                # )
                 self.set_state(state_value=99)
 
         self.set_state(state_value=2)
@@ -223,70 +270,132 @@ class ProgramItem(BaseClass):
                     device_name="turntable_steering", value=self.turntable_angle
                 )
                 time.sleep(0.1)
-            self.mcu.send_all_cmd()
+            loop = asyncio.get_event_loop()
+            # self.mcu.send_all_cmd()
+            loop.create_task(self.mcu.send_all_cmd())
 
     async def do_run(self, *args):
         await asyncio.sleep(0.001)
         # if not self.goods_art_no:  # and self.action_name != "初始化位置"
         #     return False
         start_time = time.time()
+        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:
-                # MCU运动是否有停止检查,设定超时时间
-                return
-            print(
-                "{} 检查停止时间1:{}".format(
-                    self.action_name, time.time() - start_time
-                )
-            )
-            if self.is_led:
-                self.mcu.to_deal_device(device_name="laser_position", value=1)
-            else:
-                self.mcu.to_deal_device(device_name="laser_position", value=0)
-
-            if self.shoe_overturn:
-                self.mcu.to_deal_device(device_name="overturn_steering")
-                # time.sleep(0.1)
-            if self.camera_height is not None:
-                self.mcu.to_device_move(
-                    device_name="camera_high_motor", value=self.camera_height
-                )
-                # time.sleep(0.1)
-            if self.camera_angle is not None:
-                self.mcu.to_device_move(
-                    device_name="camera_steering", value=self.camera_angle
-                )
-                # time.sleep(0.1)
-
-            if self.turntable_position is not None:
-                self.mcu.to_device_move(
-                    device_name="turntable_position_motor",
-                    value=self.turntable_position,
-                )
-                # time.sleep(0.1)
-
-            if self.turntable_angle is not None:
-                self.mcu.to_device_move(
-                    device_name="turntable_steering", value=self.turntable_angle
-                )
-                # time.sleep(0.1)
-
-            # MCU运动是否有停止检查,设定超时时间
-            await self.mcu.send_all_cmd()
-            if self.mode_type != "其他配置":
-                await asyncio.sleep(1.2)
-                print("二次检查")
-                if await self.check_mcu_move_is_stop(re_check=True) is False:
-                    print("MCU检测运动未停止,自动退出,   提前退出")
+        if self.is_move_device:
+            if settings.IS_MCU:
+                if self.mode_type != "其他配置" and await self.check_mcu_move_is_stop() is False:
+                    # MCU运动是否有停止检查,设定超时时间
                     return
+                print(
+                    "{} 检查停止时间1:{}".format(
+                        self.action_name, time.time() - start_time
+                    )
+                )
+                if self.is_led:
+                    self.mcu.to_deal_device(device_name="laser_position", value=1)
+                else:
+                    self.mcu.to_deal_device(device_name="laser_position", value=0)
+
+                if self.shoe_overturn:
+                    self.mcu.to_deal_device(device_name="overturn_steering")
+                    await asyncio.sleep(0.001)
+                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
+                    )
+                    self.last_camera_height = self.camera_height
+                    await asyncio.sleep(0.01)
+                if self.camera_angle is not None:
+                    if self.turntable_position == -40:
+                        self.mcu.to_device_move(
+                            device_name="camera_steering", value=-39.9
+                        )
+                        logger.info("转盘位置首次运动===>,%s",-39.9)
+                    elif self.turntable_position == 40:
+                        self.mcu.to_device_move(
+                            device_name="camera_steering", value=39.9
+                        )
+                        logger.info("转盘位置首次运动===>,%s",39.9)
+                    else:
+                        self.mcu.to_device_move(
+                            device_name="camera_steering", value=self.camera_angle-0.1
+                        )
+                        logger.info("转盘位置首次运动===>,%s",self.camera_angle-0.1)
+                    await asyncio.sleep(0.01)
+                    self.mcu.to_device_move(
+                        device_name="camera_steering", value=self.camera_angle
+                    )
+                    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,
+                    )
+                    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
+                    )
+                    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(0.2)
+                    print("二次检查")
+                    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)
         if self.is_photograph:
             # print("photograph==================")
             self.mcu.to_deal_device(device_name="buzzer", times=1)
             # 用于临时拍照计数
+            #  配置设置是否运动设备,不运动直接去拍照
+            if self.is_move_device:
+                if not await self.camera_check_mcu_move_is_stop(re_check=True):
+                    logger.info("拍照前运动检测失败===>,延迟0.5秒后拍摄")
+                    await asyncio.sleep(0.1)
+                # return
             is_af = True if self.af_times > 0 else False
             if self.smart_shooter != None:
                 # 拍照
@@ -295,12 +404,20 @@ class ProgramItem(BaseClass):
                 goods_art_no = self.goods_art_no
                 if record_id == -1:
                     goods_art_no = ""
-                print("smart shooter CameraShooter", record_id, goods_art_no)
+                camera_configs = settings.getSysConfigs(
+                    "camera_configs",
+                    "iso_config",
+                    None,
+                )
+                print(" camera_configs p_item",camera_configs)
+                CameraKey = camera_configs[self.point_name].get("CameraKey",None)
+                print("smart shooter CameraShooter", record_id, goods_art_no, CameraKey)
                 await self.smart_shooter.CameraShooter(
                     msg_type="run_mcu",
                     goods_art_no=goods_art_no,
                     id=record_id,
                     is_af=is_af,
+                    CameraKey=CameraKey,
                 )
                 print("smart shooter CameraShooter end")
             else:

+ 75 - 89
python/mcu/RemoteControlV2.py

@@ -234,105 +234,91 @@ class RemoteControlV2(BaseClass):
         self.msg_type = "blue_tooth"
         self.photo_take_state = 2
 
-    async def handlerTakePhoto(self, smart_shooter=None):
+    async def handlerTakePhoto(self, smart_shooter=None,session=None,record=None,PointName="A"):
         """处理单独拍照"""
         await asyncio.sleep(0.1)
-        print("开始单拍0")
-        session = SqlQuery()
-        crud = CRUD(PhotoRecord)
-        record = crud.read(session=session, order_by="id", ascending=False)
-        print("开始单拍0-读取数据库")
-        if record == None:
-            # 发送失败消息
+        print("开始单拍1")
+        if record.image_index == 19:
+            self.msg_type = "photo_take"
             self.sendSocketMessage(
                 code=1,
-                msg="单拍失败,请先输入货号或扫码进行组合拍摄",
+                msg="单拍失败,单个货号最多允许拍摄20张产品图",
                 data=None,
                 device_status=2,
             )
-        else:
-            print("开始单拍1")
-            if record.image_index == 19:
-                self.msg_type = "photo_take"
-                self.sendSocketMessage(
-                    code=1,
-                    msg="单拍失败,单个货号最多允许拍摄20张产品图",
-                    data=None,
-                    device_status=2,
-                )
-                self.msg_type = "blue_tooth"
-                return
-            deviceConfig = CRUD(DeviceConfig)
-            deviceConfigData = deviceConfig.read(session=session, conditions={"id": record.action_id})
-            select_tab_id = deviceConfigData.tab_id
-            AllTabConfig = deviceConfig.read_all(session=session, conditions={"tab_id": select_tab_id})
-            action_id = 0
-            if AllTabConfig[len(AllTabConfig) - 1].take_picture == True:
-                action_id = AllTabConfig[0].id
-            else:
-                action_id = AllTabConfig[len(AllTabConfig) - 1].id
-            image_index = record.image_index + 1
-            self.photo_take_state = 1
-            deviceConfig = CRUD(DeviceConfig)
-            deviceConfigData = deviceConfig.read(
-                session=session, conditions={"id": record.action_id}
+            self.msg_type = "blue_tooth"
+            return
+        self.photo_take_state = 1
+        deviceConfig = CRUD(DeviceConfig)
+        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
+        image_index = record.image_index + 1
+        self.photo_take_state = 1
+        state, record_id = await insert_photo_records(
+            record.image_deal_mode,
+            record.goods_art_no,
+            image_index,
+            record.action_id,
+        )
+        session.close()
+        print("开始单拍1-插入数据")
+        try:
+            camera_configs = settings.getSysConfigs(
+                "camera_configs",
+                "iso_config",
+                None,
             )
-            select_tab_id = deviceConfigData.tab_id
-            AllTabConfig = deviceConfig.read_all(
-                session=session, conditions={"tab_id": select_tab_id}
+            CameraKey = camera_configs[PointName].get("CameraKey", None)
+            print("单排CameraKey",CameraKey)
+            loop = asyncio.get_event_loop()
+            loop.create_task(
+                smart_shooter.CameraShooter(
+                    msg_type="handler_take_picture",
+                    goods_art_no=record.goods_art_no,
+                    id=record_id,
+                    CameraKey=CameraKey,
+                ),
+                name="CameraShooter",
             )
-            action_id = 0
-            if AllTabConfig[len(AllTabConfig) - 1].take_picture == True:
-                action_id = AllTabConfig[0].id
-            else:
-                action_id = AllTabConfig[len(AllTabConfig) - 1].id
-            image_index = record.image_index + 1
-            self.photo_take_state = 1
-            state, record_id = await insert_photo_records(
-                record.image_deal_mode,
-                record.goods_art_no,
-                image_index,
-                action_id,
+            await asyncio.sleep(0.8)
+            # await asyncio.sleep(1)
+            self.msg_type = "photo_take"
+            self.sendSocketMessage(
+                code=0,
+                msg="{} 执行完成~".format(
+                    "执行右脚程序"
+                    if record.image_deal_mode == 1
+                    else "执行左脚程序"
+                ),
+                data={"goods_art_no": record.goods_art_no, "id": record_id},
+                device_status=2,
             )
-            print("开始单拍1-插入数据")
-            try:
-                if smart_shooter == None:
-                    capture_one = DigiCam()
-                    watch_dog = FileEventHandler()
-                    if watch_dog.observer is None:
-                        captrure_folder_path = capture_one.getCaptureFolderPath()
-                        watch_dog.start_observer(captrure_folder_path)
-                    watch_dog.goods_art_no = record.goods_art_no
-                    watch_dog.image_index = image_index
-                    print("开始单拍1-检查相机")
-                    capture_one.run_capture_action("Capture")
-                    print("开始单拍1-完成拍照")
-                else:
-                    loop = asyncio.get_event_loop()
-                    loop.create_task(
-                        smart_shooter.CameraShooter(
-                            msg_type="handler_take_picture",
-                            goods_art_no=record.goods_art_no,
-                            id=record_id,
-                        ),
-                        name="CameraShooter",
-                    )
-                await asyncio.sleep(1)
-                self.msg_type = "photo_take"
-                self.sendSocketMessage(
-                    code=0,
-                    msg="{} 执行完成~".format(
-                        "执行右脚程序"
-                        if record.image_deal_mode == 1
-                        else "执行左脚程序"
-                    ),
-                    data={"goods_art_no": record.goods_art_no, "id": record_id},
-                    device_status=2,
-                )
-                self.msg_type = "blue_tooth"
-            except Exception as e:
-                print(f"错误:{e}")
-                self.sendSocketMessage(1, "处理失败,请重试", device_status=-1)
+            self.msg_type = "photo_take"
+            self.sendSocketMessage(
+                code=0,
+                msg="{} 执行完成~".format(
+                    "执行右脚程序"
+                    if record.image_deal_mode == 1
+                    else "执行左脚程序"
+                ),
+                data={"goods_art_no": record.goods_art_no, "id": record_id},
+                device_status=2,
+            )
+            self.msg_type = "blue_tooth"
+        except Exception as e:
+            print(f"错误:{e}")
+            self.sendSocketMessage(1, "处理失败,请重试", device_status=-1)
         self.photo_take_state = 0
 
     async def analysis_received_data(self):

+ 4 - 3
python/mcu/SerialIns.py

@@ -69,9 +69,10 @@ 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)
+                # time.sleep(0.001)
                 return True
             except:
                 self.serial_handle = None
@@ -224,7 +225,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]
@@ -283,7 +284,7 @@ class SerialIns(object):
             for i in plist:
                 print("串口列表:", i.description)
                 print("串口列表:", i.name)
-                if "CH340" in i.description:
+                if "CH34" in i.description:
                     print("CH340:", i.name)
                     self.port_name = i.name
                     print("----------", i)

+ 324 - 186
python/mcu/capture/smart_shooter_class.py

@@ -5,14 +5,17 @@ import zmq
 import asyncio
 from PIL import Image
 from io import BytesIO
-import base64,threading
+import base64, threading
 import zmq, sys, time
 from utils.SingletonType import SingletonType
 import settings
 import logging
+from databases import SqlQuery, CRUD, SysConfigs
 from utils.common import message_queue
-
+from models import SysConfigParams
 logger = logging.getLogger(__name__)
+
+
 # 定义为单例模式,避免被重复实例化
 class SmartShooter(metaclass=SingletonType):
     SET_REQ = "tcp://127.0.0.1:54544"
@@ -30,6 +33,7 @@ class SmartShooter(metaclass=SingletonType):
         self.callback_listen = None
         self.listen_init = None
         self.websocket = None
+        # self.main_loop = None
 
     def __send_tcp_message(self, socket, msg):
         # await asyncio.sleep(0.01)
@@ -50,19 +54,24 @@ class SmartShooter(metaclass=SingletonType):
         req_socket.connect(self.SET_REQ)
         return req_socket, context
 
+    async def sendMessageSocket(self, message):
+        if self.websocket_manager and self.websocket:
+            await self.websocket_manager.send_personal_message(message, self.websocket)
+
     def __create_listen(self) -> tuple[zmq.Socket, zmq.Context]:
         context = zmq.Context()
         listen_socket = context.socket(zmq.SUB)
         listen_socket.setsockopt(zmq.SUBSCRIBE, b"")
         # 设置发送超时为 5000 毫秒(5 秒)
-        listen_socket.setsockopt(zmq.RCVTIMEO, 4000)
+        listen_socket.setsockopt(zmq.RCVTIMEO, 5000)
         # 设置接收超时为 5000 毫秒(5 秒)
-        listen_socket.setsockopt(zmq.SNDTIMEO, 4000)
+        listen_socket.setsockopt(zmq.SNDTIMEO, 5000)
         listen_socket.setsockopt(zmq.LINGER, 0)  # 设置为 0 表示不等待未完成的操作
         listen_socket.connect(self.LISTEN_REQ)
         return listen_socket, context
-    async def GetCameraProperty(self):
-        '''获取相机属性'''
+
+    async def GetCameraProperty(self, CameraKey=None):
+        """获取相机属性"""
         await asyncio.sleep(0.01)
         """
             实时获取相机信息,是否连接、软件是否被打开
@@ -74,6 +83,9 @@ class SmartShooter(metaclass=SingletonType):
             req["msg_id"] = "GetCamera"
             req["msg_seq_num"] = 0
             req["CameraSelection"] = "All"
+            if CameraKey is not None:
+                req["CameraSelection"] = "Single"
+                req["CameraKey"] = CameraKey
             json_msg = self.__send_tcp_message(socket, req)
             msg_result = json_msg.get("msg_result")
             if not msg_result:
@@ -87,8 +99,8 @@ class SmartShooter(metaclass=SingletonType):
                 return False, "相机未连接"
             # 链接的相机
             CameraStatus = False
-            CameraIndex =-1
-            for cam_idx,item in enumerate(cameraInfo):
+            CameraIndex = -1
+            for cam_idx, item in enumerate(cameraInfo):
                 CameraStatus = item.get("CameraStatus") in ["Ready", "Busy"]
                 if CameraStatus == True:
                     CameraIndex = cam_idx
@@ -110,134 +122,168 @@ class SmartShooter(metaclass=SingletonType):
             context.term()
             msg_send = "相机未连接或软件未打开"
             return False, msg_send
-    async def GetCameraInfo(self, is_send=True, msg_type=""):
-        await asyncio.sleep(0.01)
-        self.msg_type = msg_type
+    def get_iso_range(self,camera_info):
+        """
+        从相机数据中获取 ISO 的可调范围
+        :param camera_data: 相机状态数据列表 (即你提供的那个长列表)
+        :return: ISO 范围列表,如果未找到则返回 None
+        """
+
+        # 获取属性信息列表
+        property_info_list = camera_info.get('CameraPropertyInfo', [])
+
+        for prop in property_info_list:
+            # 查找 CameraPropertyType 等于 'ISO' 的项
+            if prop.get('CameraPropertyType') == 'ISO':
+                return prop.get('CameraPropertyRange')
+
+        return None
+    async def GetCameraInfo(self, is_send=True, msg_type="", CameraKey=None):
+        await asyncio.sleep(0.001)
+        # self.msg_type = msg_type
         """
             实时获取相机信息,是否连接、软件是否被打开
             """
-        socket, context = self.__create_req(time_out=2)
+        socket, context = self.__create_req(time_out=5)
         try:
             req = {}
             req["msg_type"] = "Request"
             req["msg_id"] = "GetCamera"
             req["msg_seq_num"] = 0
             req["CameraSelection"] = "All"
+            if CameraKey is not None:
+                req["CameraSelection"] = "Single"
+                req["CameraKey"] = CameraKey
             json_msg = self.__send_tcp_message(socket, req)
             msg_result = json_msg.get("msg_result")
             if not msg_result:
                 self.connect_status = False
-                msg_send = "相机未连接或软件未打开"
+                msg_send =  f"请检查{CameraKey},相机是否连接" if CameraKey else f"请检查相机是否连接"
                 if is_send:
                     message = {
                         "code": 1,
                         "msg": msg_send,
                         "data": msg_result,
-                        "msg_type": self.msg_type,
+                        "CameraKey":CameraKey,
+                        "msg_type": msg_type,
                         "device_status": -1,
                     }
-                    await self.websocket_manager.send_personal_message(
-                        message, self.websocket
-                    )
+                    await self.sendMessageSocket(message)
                 return False, msg_send
             cameraInfo = json_msg.get("CameraInfo")
             if cameraInfo == None or len(cameraInfo) == 0:
                 self.connect_status = False
-                msg_send = "相机未连接"
+                msg_send = (
+                    f"请检查{CameraKey},相机是否连接"
+                    if CameraKey
+                    else f"请检查相机是否连接"
+                )
                 if is_send:
                     message = {
                         "code": 1,
                         "msg": msg_send,
                         "data": msg_result,
-                        "msg_type": self.msg_type,
+                        "msg_type": msg_type,
                         "device_status": -1,
                     }
-                    await self.websocket_manager.send_personal_message(
-                        message, self.websocket
-                    )
+                    await self.sendMessageSocket(message)
                 return False, "相机未连接"
             # 链接的相机
             CameraStatus = any(
                 item.get("CameraStatus") in ["Ready", "Busy"] for item in cameraInfo
             )
+            # 在这里处理
+            CameraLists = [
+                {
+                    "CameraSelection": item.get("CameraSelection"),
+                    "CameraKey": item.get("CameraKey"),
+                    "CameraName": item.get("CameraName"),
+                    "CameraStatus": item.get("CameraStatus") in ["Ready", "Busy"],
+                    "CameraISO": self.get_iso_range(item),
+                }
+                for item in cameraInfo
+            ]
             if not CameraStatus:
                 self.connect_status = False
-                msg_send = "相机未连接"
+                msg_send = (
+                    f"请检查{CameraKey},相机是否连接"
+                    if CameraKey
+                    else f"请检查相机是否连接"
+                )
                 if is_send:
                     message = {
                         "code": 1,
                         "msg": msg_send,
                         "data": msg_result,
-                        "msg_type": self.msg_type,
+                        "CameraKey":CameraKey,
+                        "msg_type": msg_type,
                         "device_status": -1,
                     }
-                    await self.websocket_manager.send_personal_message(
-                        message, self.websocket
-                    )
+                    await self.sendMessageSocket(message)
                 return False, msg_send
             self.connect_status = True
-            msg_send = "相机已连接"
+            msg_send = f"相机{CameraKey}已连接" if CameraKey else "相机已连接"
+            # print("CameraLists", CameraLists)
             if is_send:
                 message = {
                     "code": 0,
                     "msg": msg_send,
                     "data": msg_result,
-                    "msg_type": self.msg_type,
+                    "CameraLists":CameraLists,
+                    "msg_type": msg_type,
                     "device_status": 2,
                 }
-                await self.websocket_manager.send_personal_message(
-                    message, self.websocket
-                )
+                await self.sendMessageSocket(message)
             # print("相机已连接状态信息---->", cameraInfo)
+            self.initConfigIsoSettings(
+                CameraLists=CameraLists, isMultCameraMode=settings.USER_CAMERAS
+            )
             return True, "相机已连接"
-        except zmq.Again:
-            print("获取相机信息超时,继续监听...")
-            msg_send = "相机未连接或软件未打开"
-            if is_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
-                )
+        except zmq.Again as e:
+            self.connect_status = False
+            socket.close()
+            context.term()
+            print("获取相机信息超时,继续监听...",e)
+            msg_send =  f"请检查{CameraKey},相机是否连接" if CameraKey else f"请检查相机是否连接"
             return False, msg_send
         except Exception as e:
-            print("拍照异常", e)
+            print("相机状态获取异常", e)
             self.connect_status = False
             socket.close()
             context.term()
-            msg_send = "相机未连接或软件未打开"
+            msg_send = (
+                f"请检查{CameraKey},相机是否连接"
+                if CameraKey
+                else f"请检查相机是否连接"
+            )
             if is_send:
                 message = {
                     "code": 1,
                     "msg": msg_send,
                     "data": None,
-                    "msg_type": self.msg_type,
+                    "msg_type": msg_type,
                     "device_status": -1,
                 }
-                await self.websocket_manager.send_personal_message(
-                    message, self.websocket
-                )
+                await self.sendMessageSocket(message)
             return False, msg_send
 
-    async def SetCameraFPS(self, fps=5):
+    async def SetCameraFPS(self, fps=5, CameraKey=None):
         """
         激活相机预览
         """
-        camera_states, _ = await self.GetCameraInfo(is_send=False)
+        camera_states, _ = await self.GetCameraInfo(is_send=False, CameraKey=CameraKey)
         if not camera_states:
             return False, "请先连接相机"
+        socket, context = self.__create_req()
         try:
-            socket, context = self.__create_req()
             req = {}
             req["msg_type"] = "Request"
             req["msg_id"] = "LiveviewFPS"
             req["msg_seq_num"] = 0
             req["CameraSelection"] = "All"
+            if CameraKey is not None:
+                req["CameraSelection"] = "Single"
+                req["CameraKey"] = CameraKey
             req["CameraLiveviewFPS"] = fps
             json_msg = self.__send_tcp_message(socket, req)
             msg_result = json_msg.get("msg_result")
@@ -251,20 +297,24 @@ class SmartShooter(metaclass=SingletonType):
             context.term()
             return False, "相机未连接或软件未打开"
 
-    async def setCameraProperty(self, property="ISO", value=0):
+    async def setCameraProperty(self, property="ISO", value=0, CameraKey=None):
         # SetProperty
-        camera_states, _ = await self.GetCameraInfo(is_send=False)
+        camera_states, _ = await self.GetCameraInfo(is_send=False, CameraKey=CameraKey)
         if not camera_states:
             return False, "请先连接相机"
+        socket, context = self.__create_req()
         try:
-            socket, context = self.__create_req()
             req = {}
             req["msg_type"] = "Request"
             req["msg_id"] = "SetProperty"
             req["msg_seq_num"] = 0
             req["CameraSelection"] = "All"
+            if CameraKey is not None:
+                req["CameraSelection"] = "Single"
+                req["CameraKey"] = CameraKey
             req["CameraPropertyType"] = str(property)
             req["CameraPropertyValue"] = str(value)
+            print("发送设置属性请求", req)
             json_msg = self.__send_tcp_message(socket, req)
             msg_result = json_msg.get("msg_result")
             if not msg_result:
@@ -279,66 +329,158 @@ class SmartShooter(metaclass=SingletonType):
             msg_send = "相机未连接或软件未打开"
             return False, msg_send
 
-    async def EnableCameraPreview(self, enable_status=True, msg_type=""):
+    def getConfigIso(self, CameraKey=None):
+        """获取ISO配置信息"""
+        camera_configs = settings.getSysConfigs(
+            "camera_configs",
+            "iso_config",
+            None,
+        )
+        Itemiso = {}
+        for idx, item in enumerate(camera_configs):
+            itemConfig = camera_configs[item]
+            if itemConfig == {}:
+                continue
+            ItemCameraKey = itemConfig.get("CameraKey")
+            if ItemCameraKey == CameraKey:
+                Itemiso = itemConfig.get("iso")
+                break
+        low_iso = Itemiso.get("low", 100)
+        high_iso = Itemiso.get("high", 6400)
+        return low_iso, high_iso
+
+    def initConfigIsoSettings(self, CameraLists=[],isMultCameraMode=False):
+        if not CameraLists:
+            return None
+        """获取ISO配置信息"""
+        camera_configs = settings.getSysConfigs(
+            "camera_configs",
+            "iso_config",
+            None,
+        )
+        temp_A_point = camera_configs.get("A", None)
+        if temp_A_point is not None:
+            print("已配置无需更新")
+            itemSettings = CameraLists[0]
+            OldCameraKey = temp_A_point.get("CameraKey", None)
+            if OldCameraKey == itemSettings.get("CameraKey", None):
+                print("相机无变动。无需自动更改配置")
+                return None
+            basic_iso = temp_A_point.get("iso", {"low": 100, "high": 6400})
+            print("isMultCameraMode======>>>>",isMultCameraMode)
+            if not isMultCameraMode:
+                points = {"A": {}}
+                print("相机发生变动。需要更改配置")
+                # 如果客户是单相机版本用户,需要每次同步camera信息
+                points["A"] = {
+                    **itemSettings,
+                    "iso": basic_iso,
+                }
+                sys_iso_config = {"key": "camera_configs", "value": {"iso_config": points}}
+                sys_iso_config: SysConfigParams
+                print("单相机用户同步相机配置", sys_iso_config)
+                settings.updateSysConfigs(params=sys_iso_config)
+                # 同步本地到线上
+                settings.sync_sys_configs2Online()
+            return None
+        points = {"A": {}, "B": {}, "C": {}}
+        for idx,item in enumerate(points):
+            low_iso = camera_configs.get("low", 100)
+            high_iso = camera_configs.get("high", 6400)
+            if idx > len(CameraLists) -1:
+                points[item] = {
+                    "iso": {"low": low_iso, "high": high_iso},
+                }
+                continue
+            itemSettings = CameraLists[idx]
+            points[item] = {**itemSettings, "iso": {"low": low_iso, "high": high_iso}}
+        sys_iso_config = {"key": "camera_configs", "value": {"iso_config": points}}
+        sys_iso_config: SysConfigParams
+        print("首次初始化", sys_iso_config)
+        settings.updateSysConfigs(params=sys_iso_config)
+        # 同步本地到线上
+        settings.sync_sys_configs2Online()
+    async def EnableCameraPreview(
+        self, enable_status=True, msg_type="", CameraKey=None,isMultCameraMode=False
+    ):
+        print("收到得msg_type====>>>", msg_type)
         self.msg_type = msg_type
-        await self.SetCameraFPS(5)
+        await self.SetCameraFPS(5, CameraKey=CameraKey)
         """
         激活相机预览
         """
-        camera_states, _ = await self.GetCameraInfo(is_send=False)
+        camera_states, _ = await self.GetCameraInfo(is_send=False, CameraKey=CameraKey)
         if not camera_states:
+            msg_send = "预览启用失败"
+            message = {
+                "code": 0,
+                "msg": msg_send,
+                "data": None,
+                "msg_type": msg_type,
+                "device_status": -1,
+            }
+            logger.error(message)
+            await self.sendMessageSocket(message)
             return False, "请先连接相机"
         try:
-            camera_configs = settings.getSysConfigs(
-                "camera_configs",
-                "iso_config",
-                {"low": 100, "high": 6400, "mode": "un_auto"},
-            )
-            low_iso = camera_configs.get("low", 100)
-            high_iso = camera_configs.get("high", 6400)
+            low_iso, high_iso = self.getConfigIso(CameraKey=CameraKey)
             print("LOW_ISO", low_iso)
             print("HIGH_ISO", high_iso)
             # 等于auto就不设置
             if enable_status == True:
                 if str(high_iso).lower() != "auto":
-                    await self.setCameraProperty(property="ISO", value=str(high_iso))
+                    set_state,set_msg = await self.setCameraProperty(
+                        property="ISO", value=str(high_iso), CameraKey=CameraKey
+                    )
                 else:
                     print("high_iso 等于auto就不设置")
             if enable_status == False:
                 if str(low_iso).lower() != "auto":
-                    await self.setCameraProperty(property="ISO", value=str(low_iso))
+                    set_state,set_msg = await self.setCameraProperty(
+                        property="ISO", value=str(low_iso), CameraKey=CameraKey
+                    )
                 else:
                     print("low_iso 等于auto就不设置")
+            # print("设置状态", set_state, set_msg)
             socket, context = self.__create_req()
             req = {}
             req["msg_type"] = "Request"
             req["msg_id"] = "EnableLiveview"
             req["msg_seq_num"] = 0
             req["CameraSelection"] = "All"
+            if CameraKey is not None:
+                req["CameraSelection"] = "Single"
+                req["CameraKey"] = CameraKey
             req["Enable"] = enable_status
             json_msg = self.__send_tcp_message(socket, req)
             msg_result = json_msg.get("msg_result")
             if not msg_result:
                 self.perview_state = False
                 msg_send = "预览启用失败"
-                self.sendSocketMessage(
-                    code=0,
-                    msg=msg_send,
-                    device_status=2,
-                )
+                message = {
+                    "code": 0,
+                    "msg": msg_send,
+                    "data": None,
+                    "msg_type": msg_type,
+                    "device_status": -1,
+                }
+                logger.error(message)
+                await self.sendMessageSocket(message)
                 return False, "预览启用失败"
             msg_send = "预览启用成功" if enable_status else "预览关闭成功"
             message = {
                 "code": 0,
                 "msg": msg_send,
                 "data": None,
-                "msg_type": self.msg_type,
+                "msg_type": msg_type,
                 "device_status": 2,
             }
-            await self.websocket_manager.send_personal_message(message, self.websocket)
+            logger.error(message)
+            await self.sendMessageSocket(message)
             return True, "预览启用成功" if enable_status else "预览关闭成功"
         except zmq.Again:
             print("启动预览超时,继续监听...")
+
         except:
             self.perview_state = False
             socket.close()
@@ -348,42 +490,38 @@ class SmartShooter(metaclass=SingletonType):
                 "code": 1,
                 "msg": msg_send,
                 "data": None,
-                "msg_type": self.msg_type,
+                "msg_type": msg_type,
                 "device_status": -1,
             }
-            await self.websocket_manager.send_personal_message(message, self.websocket)
+            logger.error(message)
+            await self.sendMessageSocket(message)
             return False, "相机未连接或软件未打开"
 
-    async def CameraAutofocus(self):
+    async def CameraAutofocus(self, CameraKey=None):
         """
         相机自动对焦
         """
-        camera_states, _ = await self.GetCameraInfo(is_send=False)
+        camera_states, _ = await self.GetCameraInfo(is_send=False, CameraKey=CameraKey)
         print("CameraAutofocus 执行对焦")
         if not camera_states:
             return False, "请先连接相机"
+        socket, context = self.__create_req()
         try:
-            socket, context = self.__create_req()
             req = {}
             req["msg_type"] = "Request"
             req["msg_id"] = "Autofocus"
             req["msg_seq_num"] = 0
             req["CameraSelection"] = "All"
+            if CameraKey is not None:
+                req["CameraSelection"] = "Single"
+                req["CameraKey"] = CameraKey
+            start_time = time.time()  # 添加对焦开始时间记录
             json_msg = self.__send_tcp_message(socket, req)
+            end_time = time.time()  # 添加对焦结束时间记录
+            logger.info(f"对焦通信耗时: {end_time - start_time:.4f} 秒")
             print("json_msg", json_msg)
             msg_result = json_msg.get("msg_result")
             if not msg_result:
-                msg_send = "对焦失败"
-                message = {
-                    "code": 1,
-                    "msg": msg_send,
-                    "data": None,
-                    "msg_type": "smart_shooter_photo_take",
-                    "device_status": -1,
-                }
-                await self.websocket_manager.send_personal_message(
-                    message, self.websocket
-                )
                 return False, "对焦失败"
             return True, "对焦成功"
         except zmq.Again:
@@ -393,33 +531,60 @@ class SmartShooter(metaclass=SingletonType):
             context.term()
             return False, "相机未连接或软件未打开"
 
-    async def CameraShooter(self, msg_type="", goods_art_no="", id=0, is_af=False,delay=1.5):
+    async def CameraShooter(
+        self, msg_type="", goods_art_no="", id=0, is_af=False, delay=0.5, CameraKey=None
+    ):
         # 这里延迟一秒钟 等待前置命令完成
         await asyncio.sleep(delay)
         # 对焦混用
         if is_af:
-            await self.CameraAutofocus()
+            # await self.EnableCameraPreview(
+            #     enable_status=True,
+            #     msg_type="smart_shooter_enable_preview_status",
+            #     CameraKey=CameraKey,
+            # )
+            # start_time = time.time()
+            await self.CameraAutofocus(CameraKey=CameraKey)
+            # end_time = time.time()
+            # elapsed_time = end_time - start_time
+            # logger.info(f"自动对焦耗时  {elapsed_time:.4f} 秒")
+            # await self.EnableCameraPreview(
+            #     enable_status=False, msg_type="smart_shooter_enable_preview_status", CameraKey=CameraKey
+            # )
         self.msg_type = msg_type
         print("camera_states", msg_type)
         """
         执行拍照
         """
-        camera_states, _ = await self.GetCameraInfo(is_send=True)
+        camera_states, _ = await self.GetCameraInfo(is_send=True, CameraKey=CameraKey)
         print("camera_states CameraShooter", camera_states)
         if not camera_states:
             return False, "请先连接相机"
+        socket, context = self.__create_req()
         try:
-            socket, context = self.__create_req()
+            low_iso, high_iso = self.getConfigIso(CameraKey=CameraKey)
+            print("LOW_ISO", low_iso)
+            print("HIGH_ISO", high_iso)
+            if str(low_iso).lower() != "auto":
+                await self.setCameraProperty(
+                    property="ISO", value=str(low_iso), CameraKey=CameraKey
+                )
+            else:
+                print("low_iso 等于auto就不设置")
             req = {}
             req["msg_type"] = "Request"
             req["msg_id"] = "Shoot"
             req["msg_seq_num"] = 1
             req["CameraSelection"] = "All"
+            if CameraKey is not None:
+                req["CameraSelection"] = "Single"
+                req["CameraKey"] = CameraKey
             if goods_art_no != "" and id != 0:
                 # 此处用逗号分割,货号和id,需要在监听部分进行切割保存处理,如果使用下划线或者减号,可能货号中存在对应符号
                 req["PhotoOrigin"] = f"{goods_art_no},{id}"
             else:
                 req["PhotoOrigin"] = ""
+            print("发送拍照请求", req)
             json_msg = self.__send_tcp_message(socket, req)
             print("CameraShooter", json_msg)
             msg_result = json_msg.get("msg_result")
@@ -429,55 +594,48 @@ class SmartShooter(metaclass=SingletonType):
                     "code": 1,
                     "msg": msg_send,
                     "data": None,
-                    "msg_type": self.msg_type,
+                    "msg_type": msg_type,
                     "device_status": -1,
                 }
-                await self.websocket_manager.send_personal_message(
-                    message, self.websocket
-                )
+                await self.sendMessageSocket(message)
                 return False, msg_send
             msg_send = "拍照成功"
             message = {
                 "code": 0,
                 "msg": msg_send,
                 "data": None,
-                "msg_type": self.msg_type,
+                "msg_type": 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)
+            await self.sendMessageSocket(message)
             return True, "拍照成功"
         except zmq.Again:
-            msg_send = "相机未连接或软件未打开"
-            message = {
-                "code": 1,
-                "msg": msg_send,
-                "data": {goods_art_no: goods_art_no, "id": id},
-                "msg_type": self.msg_type,
-                "device_status": -1,
-            }
-            await self.websocket_manager.send_personal_message(message, self.websocket)
+            print("拍照超时")
+            # msg_send = "拍照失败"
+            # message = {
+            #     "code": 1,
+            #     "msg": msg_send,
+            #     "data": {goods_art_no: goods_art_no, "id": id},
+            #     "msg_type": msg_type,
+            #     "device_status": -1,
+            # }
+            # await self.sendMessageSocket(message)
             return True, "拍照失败"
-        except:
+        except Exception as e:
+            print("拍照出错",e)
             socket.close()
             context.term()
-            msg_send = "相机未连接或软件未打开"
+            msg_send = "拍照失败"
             message = {
                 "code": 1,
                 "msg": msg_send,
                 "data": None,
-                "msg_type": self.msg_type,
+                "msg_type": msg_type,
                 "device_status": -1,
             }
-            await self.websocket_manager.send_personal_message(message, self.websocket)
+            await self.sendMessageSocket(message)
             return False, msg_send
+
     async def asyncMessageListen(self):
         if self.websocket.client_state.name != "CONNECTED":
             print("WebSocket连接已断开,停止发送消息")
@@ -487,77 +645,57 @@ class SmartShooter(metaclass=SingletonType):
             if not message_queue.empty():
                 message = message_queue.get_nowait()
                 print("发送消息中。。。。。", message)
-                await self.websocket_manager.send_personal_message(
-                    message, self.websocket
-                )
+                await self.sendMessageSocket(message)
                 message_queue.task_done()
         except Exception as e:
             # 处理可能的异常,如队列为空等
             pass
 
     def connect_listen(self):
-        print("smart shooter connect_listen", self.connect_status, self.listen_init)
+        print("smart shooter connect_listen START")
         if self.connect_status == True or self.listen_init == True:
             return True
-        # 发起监听
+
         sub_socket, context = self.__create_listen()
         print("构建监听", self.connect_status)
-        logger.info("构建监听,%s", self.connect_status)
+
+        # 不再需要 self.listen_loop,我们只使用主循环
+
         try:
-            # 尝试获取当前线程的事件循环
-            try:
-                self.listen_loop = asyncio.get_running_loop()
-            except RuntimeError:
-                self.listen_loop = asyncio.new_event_loop()
-        except RuntimeError:
-            # 如果当前线程没有事件循环,则创建一个新的
-            self.listen_loop = asyncio.new_event_loop()
-            asyncio.set_event_loop(self.listen_loop)
-        while True:
-            self.listen_init = True
-            if self.callback_listen == None:
-                continue
-            try:
-                # 创建任务并立即运行,设置超时以避免阻塞
-                # future = asyncio.ensure_future(self.asyncMessageListen())
-                # 运行任务,但设置超时以避免无限等待
-                # 使用一致的事件循环运行异步任务
-                # asyncio.run(self.asyncMessageListen())
-                # 运行任务,但设置超时以避免无限等待
-                self.listen_loop.run_until_complete(self.asyncMessageListen())
-            except asyncio.TimeoutError:
-                # 超时是正常的,表示没有消息需要处理
-                pass
-            except Exception as e:
-                # 处理其他可能的异常
-                print(f"Error handling async message-asyncMessageListen: {e}")
-            # camera_states, camera_msg = await self.GetCameraInfo(is_send=False)
-            # if not camera_states:
-            #     print("相机未连接回调打印", camera_states, camera_msg)
-            #     await asyncio.sleep(0.01)  # 等待相机连接
-            #     continue
-            if self.stop_listen:
-                break
-            try:
-                self.connect_status = True
-                raw = sub_socket.recv()
-                str_msg = raw.decode("utf-8")
-                json_msg = json.loads(str_msg)
-                if json_msg["msg_id"] == "NetworkPing":
+            while True:
+                self.listen_init = True
+
+                if self.stop_listen:
+                    break
+
+                # 1. 阻塞接收 ZMQ 消息 (这是唯一的阻塞点,但在子线程,所以没问题)
+                try:
+                    raw = sub_socket.recv()  # 这里会阻塞直到有消息或超时
+                    str_msg = raw.decode("utf-8")
+                    json_msg = json.loads(str_msg)
+
+                    if json_msg.get("msg_id") == "NetworkPing":
+                        continue
+
+                    # 2. 将回调提交到【主事件循环】
+                    if hasattr(self, "main_loop") and self.main_loop:
+                        # 非阻塞提交,立即返回
+                        asyncio.run_coroutine_threadsafe(
+                            self.callback_listen(json_msg), self.main_loop
+                        )
+                    else:
+                        print("Error: main_loop not set in SmartShooter")
+
+                except zmq.Again:
+                    # 超时,继续循环
                     continue
-                # self.callback_listen(json_msg)
-                asyncio.run(self.callback_listen(json_msg))
-            except zmq.Again:
-                print("接收超时,继续监听...")
-                logger.info("接收超时,继续监听...")
-                continue
-            except Exception as e:
-                self.connect_status = False
-                print(f"发生错误: {e}")
-                break
-        self.listen_init = False
-        self.connect_status = False
-        self.stop_listen = False
-        sub_socket.close()
-        context.term()
-        print("smart shooter连接断开")
+                except Exception as e:
+                    print(f"ZMQ Error: {e}")
+                    break
+
+        finally:
+            self.listen_init = False
+            self.connect_status = False
+            sub_socket.close()
+            context.term()
+            print("smart shooter连接断开")

+ 124 - 0
python/mcu_test.py

@@ -0,0 +1,124 @@
+import time
+
+import serial
+import serial.tools.list_ports
+# from mcu.base_mode.base import *
+from mcu.SerialIns import SerialIns
+import asyncio
+import random
+
+class Main():
+    def __init__(self):
+        port_name = self.list_serial_ports()
+        if not port_name:
+            return
+        self.total_n = 0
+        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 "CH34" in port.description:
+                return port.name
+        return False
+
+    def run(self):
+        n = 0
+        value = 0
+        while True:
+            value += 1
+            random_int = random.randint(1, 10)
+            time.sleep(random_int)
+            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):
+        print("开始打印数据")
+        while 1:
+            time.sleep(0.01)
+            r_data = self.serial_ins.read_cmd()
+            # print("接收数据:{}".format(r_data))
+            if r_data:
+                self.total_n += 1
+                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:{},total:{}".format(value, self.total_n))
+                if value == 1 or value == self.last_value + 1:
+                    self.last_value = value
+                else:
+                    print("数据接收有中断")
+                    raise "数据接收有中断"
+            else:
+                # print("数据未获取到")
+                pass
+
+if __name__ == '__main__':
+    main =  Main()
+    async def run_tasks():
+        loop = asyncio.get_event_loop()
+        
+        # 创建任务
+        task1 = loop.run_in_executor(None, main.run)
+        task2 = loop.run_in_executor(None, main.print_all)
+        
+        # 等待两个任务(它们会无限运行,除非发生异常)
+        await asyncio.gather(task1, task2)
+    
+    try:
+        asyncio.run(run_tasks())
+    except KeyboardInterrupt:
+        print("程序被用户中断")

+ 131 - 0
python/mcu_test_2.py

@@ -0,0 +1,131 @@
+import random
+import time
+
+import serial
+import serial.tools.list_ports
+from mcu.SerialIns import SerialIns
+import threading
+
+
+class Main():
+    def __init__(self):
+        port_name = self.list_serial_ports()
+        if not port_name:
+            return
+        self.last_value = 0
+        self.total_n = 0
+        self.serial_ins = SerialIns(port_name=port_name, baud=115200)
+
+    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 "CH34" in port.description:
+                return port.name
+        return False
+    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 run(self):
+        t = threading.Thread(target=self.print_all)
+        t.start()
+
+        n = 0
+        value = 0
+        while True:
+            value += 1
+            # time.sleep(0.02)
+            v_t = random.randint(1, 10)
+            print("倒计时 v_t:{}".format(v_t))
+            time.sleep(0.02)
+
+            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 print_all(self):
+        while 1:
+            time.sleep(0.01)
+            r_data = self.serial_ins.read_cmd()
+            if r_data:
+                self.total_n += 1
+                # print("g buf:{}".format(self.serial_ins.change_hex_to_int(r_data)))
+                if r_data[0] == 100:
+                    self.print_mcu_error_data(r_data)
+                    raise 1111
+                if r_data[1] != 1:
+                    # raise 222222
+                    continue
+
+                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:{},total:{}".format(value, self.total_n))
+                if value > 300:
+                    self.last_value += 1
+                    continue
+                if value == 1 or value == self.last_value + 1:
+                    self.last_value = value
+                else:
+                    raise "数据接收有中断"
+    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_mcu_error_data(self, receive_data):
+        # 扫码数据
+        try:
+            data = receive_data[1:].decode()
+            print("126  print_mcu_error_data:", data)
+        except BaseException as e:
+            print("128 error {}".format(e))
+        return
+
+
+if __name__ == '__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)

+ 2 - 0
python/model/device_config.py

@@ -24,6 +24,8 @@ class DeviceConfig(SQLModel, table=True):
     after_delay: Optional[float] = Field(
         default=None, description="拍照后延迟;步长1;最小0;最大99"
     )
+    point_name: Optional[str] = Field(default="A", description="点位名称,默认A点")
+    is_move_device: Optional[bool] = Field(default=True, description="是否移动设备,默认移动设备")
     led_switch: Optional[bool] = Field(default=False, description="Led灯光开关;")
     is_wait: Optional[bool] = Field(default=False, description="没用;")
     is_need_confirm: Optional[bool] = Field(default=False, description="没用;")

+ 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)

+ 41 - 3
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="请求方法")
@@ -51,6 +51,10 @@ class SaveDeviceConfig(BaseModel):
     led_switch: Optional[bool] = Field(default=None)
     is_wait: Optional[bool] = Field(default=False)
     is_need_confirm: Optional[bool] = Field(default=False)
+    point_name: Optional[str] = Field(default="A", description="点位名称,默认A点")
+    is_move_device: Optional[bool] = Field(
+        default=True, description="是否移动设备,默认移动设备"
+    )
 
 
 class PhotoRecordDelete(BaseModel):
@@ -77,7 +81,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):
@@ -98,6 +103,7 @@ class HandlerDetail(BaseModel):
     temp_list: list[TemplateItem] = Field(default=[], description="所有模板列表")
     logo_path: Optional[str] = Field(default="", description="logo地址路径")
     is_only_cutout: Optional[int] = Field(default=0, description="是否仅抠图;0否;1是")
+    is_check: Optional[int] = Field(default=0, description="是否仅检测;0否;1是")
     online_stores: Optional[list[str]] = Field(
         default=[], description="上传的店铺,数组形式"
     )
@@ -144,4 +150,36 @@ class SyncLocalConfigs(BaseModel):
     """同步系统配置"""
 
     token: str = Field(default=None, description="用户token")
-    
+    env: str = Field(default="dev", description="当前环境")
+    camera_counts: bool = Field(default=False, 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="当前环境")
+    camera_counts: bool = Field(default=False, description="相机数量")
+
+
+class RenameShadow(BaseModel):
+    """重命名阴影文件"""
+    goods_art_nos: list[str] = Field(default=None, description="货号数组")
+
+
+class ImportDirs(BaseModel):
+    """重命名阴影文件"""
+    dir_path: str = Field(default=None, description="货号数组")
+    goods_art_nos:list[str] = Field(default=["BH73323",'BH94727'], description="货号数组")

+ 40 - 3
python/service/auto_deal_pics/base_deal.py

@@ -168,8 +168,45 @@ class BaseDealImage(object):
             return {'code': 1, 'msg': '图片位置与顺序重复,请检查您的输入'}
 
         for val in imageOrderList:
-            if val not in ["俯视", "侧视", "后跟", "鞋底", "内里", "组合", "组合2", "组合3", "组合4", "组合5"]:
-                return {'code': 1, 'msg': '可选项为:俯视,侧视,后跟,鞋底,内里,组合,组合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": f"可选项为:{image_orders_str}",
+                }
 
         if resize_image_view not in imageOrderList:
             return {'code': 1, 'msg': '缩小的步骤必须是你填写的图片顺序中'}
@@ -896,7 +933,7 @@ class BaseDealImage(object):
         total_num = len(original_photo_list)
         # 当天日期作为文件夹
         seconds = time.time()
-        output_path = "output/{f_name}".format(f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
+        output_path = "{output}/{f_name}".format(output=settings.OUTPUT_DIR,f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
 
         # 遍历每个匹配好的数据进行处理
         n = 0

+ 10 - 7
python/service/base.py

@@ -8,7 +8,7 @@ import shutil
 from hashlib import sha256, md5
 import requests
 from datetime import datetime
-
+from settings import OUTPUT_DIR,handle_remove_readonly
 
 # 获取digicam的路径
 def check_install_path(other):
@@ -174,7 +174,7 @@ def get_image_mask(path):
 # 删除文件夹下的所有文件
 def remove_all_file(directory):
     try:
-        shutil.rmtree(directory)
+        shutil.rmtree(directory,onerror=handle_remove_readonly)
         os.makedirs(directory)
     except Exception as e:
         print(f'Failed to clear directory {directory}. Reason: {e}')
@@ -240,16 +240,19 @@ def check_move_goods_art_no_folder(path, goods_art_nos,limit_folder):
         for folder_data in temp_folder_list:
             # folder_path = folder_data["folder_path"]
             folder_name = folder_data["folder_name"]
-            _p = "output/{}/{}/原始图".format(folder_name, goods_art_no)
+            _p = "{}/{}/{}/原始图".format(OUTPUT_DIR,folder_name, goods_art_no)
             if os.path.exists(_p):
-                folder_data["folder_path"] = f"output/{folder_name}/{goods_art_no}"
+                folder_data["folder_path"] = f"{OUTPUT_DIR}/{folder_name}/{goods_art_no}"
                 # 整个目录移动到目标目录
                 folder_list[goods_art_no] = folder_data
                 if not os.path.exists(f"{limit_folder}/{goods_art_no}"):
                     # 目标不存在
-                    folder_list[goods_art_no] = folder_data
-                    print("移动目录", folder_data["folder_path"], limit_folder)
-                    shutil.move(folder_data["folder_path"], limit_folder)
+                    try:
+                        shutil.move(folder_data["folder_path"], limit_folder)
+                        folder_list[goods_art_no] = folder_data
+                        print("移动目录", folder_data["folder_path"], limit_folder)
+                    except:
+                        continue
                 else:
                     # 如果希望覆盖
                     print(f"目标目录 {limit_folder}/{goods_art_no} 已存在,跳过移动")

+ 186 - 87
python/service/base_deal.py

@@ -23,9 +23,8 @@ import requests
 import copy, asyncio
 from settings import sendSocketMessage
 from utils.common import message_queue
-
-
-def sendAsyncMessage(msg="", goods_arts=[], status=""):
+from logger import logger
+def sendAsyncMessage(msg="", goods_arts=[], status="",progress={}):
     """异步发送消息"""
     data = {
         "code": 0,
@@ -35,6 +34,15 @@ def sendAsyncMessage(msg="", goods_arts=[], status=""):
             "status": status,
             "goods_art_nos": goods_arts,
         },
+        "progress":{
+                "msg_type":"segment_progress",
+                "name":"抠图",
+                "goods_art_no":progress.get("goods_art_no",""),
+                "status":progress.get("status"),
+                "current":progress.get("current",0),
+                "total":progress.get("total",0),
+                "error":progress.get("error",0)
+            },
         "msg_type": "segment_progress",
     }
     message_queue.put_nowait(data)
@@ -66,6 +74,20 @@ class BaseDealImage(object):
         logo_path=None,
         image_order_list=None,
     ):
+        """
+        执行主流程处理
+        
+        Returns:
+            dict: {
+                'success': bool,  # 是否全部成功
+                'successful_folders': list,  # 成功的货号列表
+                'failed_folders': list,  # 失败的货号列表
+                'successful_num': int,  # 成功数量
+                'error_num': int  # 失败数量
+            }
+        """
+        from logger import logger
+        
         # 对所有缺失已抠图的进行抠图处理
         self.run_cutout_image(
             all_goods_art_no_folder_data=all_goods_art_no_folder_data,
@@ -75,6 +97,9 @@ class BaseDealImage(object):
         )
         error_num = 0
         successful_num = 0
+        successful_folders = []
+        failed_folders = []
+        
         for goods_art_no_folder_data in all_goods_art_no_folder_data:
             if goods_art_no_folder_data["label"] != "待处理":
                 continue
@@ -83,6 +108,8 @@ class BaseDealImage(object):
                     break
             folder_name = goods_art_no_folder_data["folder_name"]
             callback_func("开始处理文件夹==========  {} ".format(folder_name))
+            
+            flag = None
             if settings.IS_TEST:
                 flag = self.shoes_run_one_folder_to_deal(
                     goods_art_no_folder_data=goods_art_no_folder_data,
@@ -92,15 +119,6 @@ class BaseDealImage(object):
                     callback_func=callback_func,
                     windows=windows,
                 )
-                if flag is None:
-                    callback_func("货号:{} 数据异常".format(folder_name))
-                else:
-                    if flag:
-                        successful_num += 1
-                        callback_func("货号:{} 图片生成处理成功".format(folder_name))
-                    else:
-                        error_num += 1
-                        callback_func("货号:{} 图片生成处理失败".format(folder_name))
             else:
                 try:
                     flag = self.shoes_run_one_folder_to_deal(
@@ -111,28 +129,48 @@ class BaseDealImage(object):
                         callback_func=callback_func,
                         windows=windows,
                     )
-                    if flag is None:
-                        callback_func("货号:{} 数据异常".format(folder_name))
-                    else:
-                        if flag:
-                            successful_num += 1
-                            callback_func(
-                                "货号:{} 图片生成处理成功".format(folder_name)
-                            )
-                        else:
-                            error_num += 1
-                            callback_func(
-                                "货号:{} 图片生成处理失败".format(folder_name)
-                            )
                 except BaseException as e:
                     error_num += 1
                     import traceback
-
                     traceback.print_exc()
+                    logger.error(f"货号 {folder_name} 处理异常: {e}")
                     callback_func(
                         "货号:{} 图片生成处理异常,原因:{}".format(folder_name, e)
                     )
-        callback_func("处理成功:{}个,失败:{}".format(successful_num, error_num))
+                    failed_folders.append(goods_art_no_folder_data)
+                    continue
+            
+            # 判断处理结果
+            if flag is None:
+                callback_func("货号:{} 数据异常".format(folder_name))
+                error_num += 1
+                failed_folders.append(goods_art_no_folder_data)
+            else:
+                if flag:
+                    successful_num += 1
+                    callback_func("货号:{} 图片生成处理成功".format(folder_name))
+                    successful_folders.append(goods_art_no_folder_data)
+                else:
+                    error_num += 1
+                    callback_func("货号:{} 图片生成处理失败".format(folder_name))
+                    failed_folders.append(goods_art_no_folder_data)
+        
+        callback_func("处理成功:{}个,失败:{}个".format(successful_num, error_num))
+        
+        # 返回详细的处理结果
+        result = {
+            'success': error_num == 0,
+            'successful_folders': successful_folders,
+            'failed_folders': failed_folders,
+            'successful_num': successful_num,
+            'error_num': error_num
+        }
+        
+        logger.info(f"[主流程完成] 成功: {successful_num}, 失败: {error_num}")
+        if failed_folders:
+            logger.warning(f"[主流程完成] 失败货号列表: {[f['folder_name'] for f in failed_folders]}")
+        
+        return result
 
     def checkImageAmount(
         self, image_dir: str, amount: int, todo_goods_art_no_folder_name_list=None
@@ -230,11 +268,12 @@ 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 = [
                 "俯视",
                 "侧视",
                 "后跟",
@@ -245,10 +284,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:
@@ -279,6 +342,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
             # _ = ["俯视", "侧视", "后跟", "鞋底", "内里"]
@@ -330,14 +395,22 @@ class BaseDealImage(object):
         is_image_deal_mode = 0
 
         # 删除目录再新建
-        if os.path.exists("{}/阴影图处理".format(folder_path)):
-            shutil.rmtree("{}/阴影图处理".format(folder_path))
+        try:
+          if os.path.exists("{}/阴影图处理".format(folder_path)):
+            shutil.rmtree("{}/阴影图处理".format(folder_path),onerror=settings.handle_remove_readonly)
+        except Exception as e:
+          print('An exception occurred')
+          logger.info(f"base deal 抠图前目录删除出现问题:{str(e)}")
 
         self.crate_all_folders(folder_path)
         print(
             "***************all_original_images*********************",
             all_original_images,
         )
+        is_flip_800image = settings.getSysConfigs(
+            "basic_configs", "is_flip_800image", 1
+        )
+        image_deal_mode = int(is_flip_800image)
         for image_dict in all_original_images:
             if windows:
                 if windows.state != 1:
@@ -361,13 +434,14 @@ class BaseDealImage(object):
                 f"*****************此处判断鞋子是否为左右脚====>{image_index}<======={is_image_deal_mode}=======>**********************"
             )
             # 此处判断鞋子是否为左右脚
-            if image_index == 1:
-                is_image_deal_mode = 0
-                print("开始识别左右脚=========>")
-                if OnePicDeal(self.token).check_shoe_is_right(
-                    image_path=original_move_bg_image_path
-                ):
-                    is_image_deal_mode = 1  # 1表示要镜像,0表示不做镜像
+            if image_deal_mode == 1:
+                if image_index == 1:
+                    is_image_deal_mode = 0
+                    print("开始识别左右脚=========>")
+                    if OnePicDeal(self.token).check_shoe_is_right(
+                        image_path=original_move_bg_image_path
+                    ):
+                        is_image_deal_mode = 1  # 1表示要镜像,0表示不做镜像
             print(
                 "*************************进行800image 生成********************************************"
             )
@@ -426,21 +500,28 @@ class BaseDealImage(object):
                 )
             print("**********123456********************")
             curve_mask = True if "俯视" in image_order_list else False
-            if not generate_pic.run(
-                image_path=original_image_path,
-                cut_image_path=original_move_bg_image_path,
-                out_path=out_path,
-                image_deal_mode=is_image_deal_mode,
-                resize_mode=resize_mode,
-                out_pic_size=out_pic_size,
-                is_logo=True if i_n == 1 else False,
-                out_process_path_1=out_process_path_1,
-                out_process_path_2=out_process_path_2,
-                max_box=max_box,
-                logo_path=logo_path,
-                curve_mask=curve_mask,
-            ):
-                print("**********222222222222222222222222222********************")
+            try:
+                if not generate_pic.run(
+                    image_path=original_image_path,
+                    cut_image_path=original_move_bg_image_path,
+                    out_path=out_path,
+                    image_deal_mode=is_image_deal_mode,
+                    # resize_mode=resize_mode,
+                    resize_mode=1,#将这里得缩放模式改为强制不缩放 2025-10-22
+                    out_pic_size=out_pic_size,
+                    is_logo=True if i_n == 1 else False,
+                    out_process_path_1=out_process_path_1,
+                    out_process_path_2=out_process_path_2,
+                    max_box=max_box,
+                    logo_path=logo_path,
+                    curve_mask=curve_mask,
+                ):
+                    print("**********222222222222222222222222222********************")
+                    is_successful = False
+            except Exception as e:
+                import traceback
+                logger.error(f"货号 {folder_name} - 图片 {file_name} 生成失败: {e}\n{traceback.format_exc()}")
+                callback_func(f"货号 {folder_name} - 图片 {file_name} 处理异常: {e}")
                 is_successful = False
         if is_successful:
             return True
@@ -508,8 +589,15 @@ class BaseDealImage(object):
             goods_art_item["folder_name"]
             for goods_art_item in all_goods_art_no_folder_data
         ]
+        total_progress = len(all_goods_art_no_folder_data)
+        finish_progress = 0
+        error_progress = 0
+        progress = {"status":"正在处理",
+                "current":finish_progress,
+                "total":total_progress,
+                "error":error_progress}
         sendAsyncMessage(
-            msg="开始处理抠图", goods_arts=goods_art_nos, status="开始处理"
+            msg="开始处理抠图", goods_arts=goods_art_nos, status="开始处理",progress=progress
         )
         error_goods_art_no_folder = []
         for goods_art_no_folder_data in all_goods_art_no_folder_data:
@@ -521,10 +609,17 @@ class BaseDealImage(object):
             images = [x for x in self.list_dir("{}/原始图".format(folder_path))]
             cutImageList = []
             goods_art_floder_name = goods_art_no_folder_data["folder_name"]
+            progress = {"status":"正在处理",
+                "current":finish_progress,
+                "total":total_progress,
+                "error":error_progress,
+                "goods_art_no":goods_art_floder_name
+                }
             sendAsyncMessage(
                 msg="正在抠图",
                 goods_arts=[goods_art_floder_name],
                 status="处理中",
+                progress=progress
             )
             for pic_file_name in images:
                 if windows:
@@ -572,11 +667,6 @@ class BaseDealImage(object):
                                     callback_func(
                                         "货号图{} 抠图处理超时~".format(file_name)
                                     )
-                                    # sendAsyncMessage(
-                                    #     msg="抠图处理超时",
-                                    #     goods_arts=[file_name],
-                                    #     status="抠图处理超时",
-                                    # )
                                     error_goods_art_no_folder.append(folder_path)
                                     im = None
                                 except BaseException as e:
@@ -586,11 +676,6 @@ class BaseDealImage(object):
                                         )
                                     )
                                     error_goods_art_no_folder.append(folder_path)
-                                    # sendAsyncMessage(
-                                    #     msg=f"抠图处理失败,原因{e}",
-                                    #     goods_arts=[file_name],
-                                    #     status="抠图处理失败",
-                                    # )
                                     im = None
 
                             if not im:
@@ -598,24 +683,31 @@ class BaseDealImage(object):
                                     "货号图{} 抠图处理失败~".format(file_name)
                                 )
                                 error_goods_art_no_folder.append(folder_path)
-                                # sendAsyncMessage(
-                                #     msg=f"抠图处理失败",
-                                #     goods_arts=[file_name],
-                                #     status="抠图处理失败",
-                                # )
                                 continue
                             else:
-                                # sendAsyncMessage(
-                                #     msg=f"完成抠图处理",
-                                #     goods_arts=[file_name],
-                                #     status="完成抠图处理",
-                                # )
                                 callback_func("货号图{} 抠图完成~".format(file_name))
+            progress = {
+                "status":"正在处理",
+                "current":finish_progress,
+                "total":total_progress,
+                "error":error_progress,
+                "goods_art_no":goods_art_floder_name
+                }
             if goods_art_floder_name not in error_goods_art_no_folder:
+                finish_progress+=1
                 sendAsyncMessage(
-                    msg="抠图完成",
+                    msg="正在处理",
                     goods_arts=[goods_art_floder_name],
-                    status="抠图完成",
+                    status="正在处理",
+                    progress=progress
+                )
+            else:
+                progress["status"] = "处理失败"
+                sendAsyncMessage(
+                    msg="处理失败",
+                    goods_arts=[goods_art_floder_name],
+                    status="处理失败",
+                    progress=progress
                 )
             if cutout_mode == "2":
                 dealCutout = DealCutout(windows=None, token=self.token)
@@ -623,14 +715,17 @@ class BaseDealImage(object):
                 dealCutout.run()
                 while True:
                     time.sleep(0.5)
-                    # if windows:
-                    #     if windows.state != 1:
-                    #         break
                     if dealCutout.state == 3:
                         if len(dealCutout.resultData) != len(cutImageList):
                             error_goods_art_no_folder.append(folder_path)
                         break
-
+        error_progress = len(error_goods_art_no_folder)
+        progress = {
+                "status":"处理完成",
+                "current":finish_progress,
+                "total":total_progress,
+                "error":error_progress
+                }
         if error_goods_art_no_folder:
             print("以下货号抠图失败~\n {}".format(error_goods_art_no_folder))
             callback_func("以下货号抠图失败~\n {}".format(error_goods_art_no_folder))
@@ -638,14 +733,17 @@ class BaseDealImage(object):
                 msg=f"抠图处理失败",
                 goods_arts=error_goods_art_no_folder,
                 status="抠图处理失败",
+                # progress=progress
             )
         else:
-            sendAsyncMessage(
-                msg=f"完成抠图处理",
-                goods_arts=goods_art_nos,
-                status="完成抠图处理",
-            )
-            callback_func("完成抠图处理")
+            pass
+        progress["status"] = "处理失败" if error_progress == total_progress else "处理完成"
+        # sendAsyncMessage(
+        #             msg="抠图完成",
+        #             goods_arts=[],
+        #             status="抠图完成",
+        #             progress=progress
+        #         )
 
     def checkCutoutImage(self, image_dir: str, todo_goods_art_no_folder_name_list=None):
         """
@@ -1143,6 +1241,7 @@ class BaseDealImage(object):
             order_by="id",
             ascending=True,
         )
+        session.close()
         if result:
             return result.goods_art_no, result.image_index, result.image_deal_mode
         else:

+ 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

+ 49 - 21
python/service/data.py

@@ -128,12 +128,17 @@ class DataModeGenerateDetail(DataBaseModel):
         print("goods_art_no_all_data===========>>>>>", "")
         print("goods_art_no_all_data===========>>>>>", "")
         print("goods_art_no_all_data===========>>>>>", "")
-        print("goods_art_no_all_data===========>>>>>", "")
+        print("goods_art_no_all_data===========>>>>>   111   ", return_dict)
         for folder_name, value in return_dict.items():
+            print("folder name ",value)
+            print("folder name ",folder_name)
+            print("folder name ",str(value["name"]))
+            print("folder name ",str(value["name"]) , goods_art_no_all_data)
+            print("=================================================")
             if value["type"] == "goods_art_no":
-                if value["name"] in goods_art_no_all_data:
+                if str(folder_name) in goods_art_no_all_data:
                     return_dict[folder_name]["data"] = goods_art_no_all_data[
-                        value["name"]
+                       folder_name
                     ]
 
         # ------------请求编码数据----------------------
@@ -145,47 +150,62 @@ class DataModeGenerateDetail(DataBaseModel):
         )
         for folder_name, value in return_dict.items():
             if value["type"] == "goods_number":
-                if value["name"] in goods_number_all_data:
+                if str(folder_name) in goods_number_all_data:
                     return_dict[folder_name]["data"] = goods_number_all_data[
-                        value["name"]
+                        str(folder_name)
                     ]
-
+        print("goods_art_no_all_data===========>>>>>  22222   ", return_dict)
         # 清空没有值的数据
         error_key = []
         for folder_name, value in return_dict.items():
             if not value["data"]:
                 error_key.append(folder_name)
-
         if error_key:
             for folder_name in error_key:
                 return_dict.pop(folder_name)
 
         return {"code": 0, "message": "", "data": return_dict}
-
+    def makeFakeData(self, folder_name_list):
+        fakeData = {}
+        for folder in folder_name_list:
+            fakeData[folder] = {
+                "type": "goods_art_no",
+                "name": str(folder).upper(),
+                "商品货号": str(folder),
+                "data": {
+                    "商品货号": str(folder),
+                    "款号": str(folder),
+                    "性别": "女",
+                    "颜色名称": "白色",
+                    "商品面料": "pvc",
+                    "商品内里": "牛皮",
+                    "商品鞋底": "pvc",
+                },
+            }
+        return {"code": 0, "message": "", "data": fakeData}
     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)
 
         need_df = need_df.fillna(value="")
 
         # 打开表格并进行匹配
-        _df = pd.read_excel(excel_path, sheet_name=0, header=0)
+        _df = pd.read_excel(excel_path, sheet_name=0, header=0, dtype=str)  # 强制将所有列读取为字符串类型
         # 去重数据
-        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=["文件夹名称"],
+            on=["商品货号"],
             how="left",
             indicator=False,
         )
@@ -202,11 +222,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(),
                     }
@@ -214,10 +234,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(),
                     }
@@ -349,6 +369,10 @@ class DataModeGenerateDetail(DataBaseModel):
         )
         group_local_df = group_local_df.reset_index()
 
+        # -----------统一款号列的数据类型为字符串,避免merge时报错
+        group_local_df["款号"] = group_local_df["款号"].astype(str)
+        original_df["款号"] = original_df["款号"].astype(str)
+
         # -----------数据匹配关联,左关联
         group_local_df = pd.merge(
             group_local_df,
@@ -442,6 +466,10 @@ class DataModeGenerateDetail(DataBaseModel):
             subset=["款号", "颜色名称"], keep="first", inplace=True
         )
 
+        # ------------统一款号列的数据类型为字符串,避免merge时报错
+        local_df["款号"] = local_df["款号"].astype(str)
+        new_df["款号"] = new_df["款号"].astype(str)
+
         # ------------excel表格关联 上述的款号 进行标记,其他删除
         _df = new_df.drop_duplicates(subset=["款号"], keep="first", inplace=False)
         _df = _df[["款号"]]

+ 112 - 14
python/service/deal_image.py

@@ -1,4 +1,4 @@
-import os
+import os,re
 
 from  natsort import natsorted,ns
 import shutil
@@ -13,6 +13,8 @@ import requests
 from service.pic_deal import Picture
 import xlsxwriter
 from PIL import Image
+
+from utils.utils_func import check_path
 from .base_deal import BaseDealImage
 from middleware import UnicornException
 _Type = ['.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF', ".jpge", ".JPGE"]
@@ -30,8 +32,10 @@ class DealImage(BaseDealImage):
         return True
     def list_dir(self, path):
         listdir = os.listdir(path)
-        return natsorted(listdir, alg=ns.PATH)
-
+        sort_result = sorted(listdir, key=lambda x: int(re.findall(r'^(\d+)_', x)[0]) if re.findall(r'^(\d+)_', x) else 0)
+        # print("listdir   排序排序排序排序排序排序", sort_result)
+        # return natsorted(listdir, alg=ns.PATH)
+        return sort_result
     def get_date_time_original(self, file_path):
         with open(file_path, 'rb') as file_data:
             tags = exifread.process_file(file_data)
@@ -50,15 +54,34 @@ class DealImage(BaseDealImage):
         configModel = CRUD(PhotoRecord)
         result = configModel.read(
             session,
-            conditions={"goods_art_no": goods_art_no},
+            conditions={"goods_art_no": goods_art_no,"delete_time": None},
             order_by="id",
             ascending=True,
         )
+        session.close()
         if result:
             return result.goods_art_no, result.image_index, result.image_deal_mode
         else:
             return None
+    def get_goods_art_no_files(self, goods_art_no):
+        # time_array = time.strptime(date_time_original, "%Y:%m:%d %H:%M:%S")
 
+        # time_array = time.mktime(time_array)
+        # datetime_obj = datetime.datetime.fromtimestamp(time_array)
+
+        session = SqlQuery()
+        configModel = CRUD(PhotoRecord)
+        result = configModel.read_all(
+            session,
+            conditions={"goods_art_no": goods_art_no,"delete_time": None},
+            order_by="id",
+            ascending=True,
+        )
+        session.close()
+        if result:
+            return [{"image_path":item.image_path,"id":item.id,"image_index":item.image_index} for item in result]
+        else:
+            return []
     def get_data_from_hqt_with_goods_art_no(self, goods_art_no_list):
         _goods_art_no_list = copy.deepcopy(goods_art_no_list)
         _list = []
@@ -173,7 +196,35 @@ class DealImage(BaseDealImage):
             shutil.move(old_image_path, original_image_path)
         except Exception as e:
             print(f"文件操作异常:{e}")
+    def copy_images(self, goods_art_no, goods_art_no_path, old_image_path):
+        """
+        步骤:
+        1、移动到原始图
+        Args:
+            goods_art_no:
+            goods_art_no_path:
+            old_image_path:
 
+        Returns:
+
+        """
+        # 移动到原始图
+        file = os.path.split(old_image_path)[1]
+        # 扩展名
+        e = os.path.splitext(file)[1]
+        print("self.goods_images_count_dict", self.goods_images_count_dict)
+        # 获取图片序列
+        self.goods_images_count_dict[goods_art_no] += 1
+        # A9999(1).jpg
+        new_file_name = "{}({})".format(goods_art_no, self.goods_images_count_dict[goods_art_no])
+        check_path("{}/原始图".format(goods_art_no_path))
+        original_image_path = "{}/原始图/{}{}".format(goods_art_no_path, new_file_name, e)
+        try:
+            shutil.copy2(old_image_path, original_image_path)
+            return True,"移动成功"
+        except Exception as e:
+            print(f"文件操作异常:{e}")
+            return False,f"货号[{goods_art_no}]原始图{old_image_path}不存在或已被删除"
     def dealMoveImage(self, image_dir: str, callback_func=None,goods_art_no=None) -> dict:
         if not self.check_path(image_dir=image_dir + "/历史"):
             raise UnicornException("文件夹创建失败")
@@ -181,7 +232,7 @@ class DealImage(BaseDealImage):
         # 遍历目标文件夹,获取有拍摄信息的图片,并按拍摄时间排序
         files = self.list_dir(image_dir)
         original_photo_list = []  # 原始图片列表
-        for file in files:
+        for file_idx,file in enumerate(files):
             # -----图片清洗
             file_path = image_dir + "/" + file
             if os.path.isdir(file_path):  # 忽略文件夹
@@ -198,12 +249,12 @@ class DealImage(BaseDealImage):
                 if _data:
                     # 能匹配上数据库
                     goods_art_no, image_index, image_deal_mode = _data
-                    print("832 与数据库匹配goods_art_no", file_name, date_time_original, goods_art_no)
+                    print("832 与数据库匹配goods_art_no", file_path,file_name, date_time_original, goods_art_no)
                     original_photo_list.append({"file_path": file_path,
                                                 "file": file,
                                                 "date_time_original": date_time_original,
                                                 "goods_art_no": goods_art_no,
-                                                "image_index": image_index,
+                                                "image_index": file_idx,
                                                 "real_goods_art_no": "",
                                                 "real_goods_number": "",
                                                 })
@@ -221,20 +272,22 @@ class DealImage(BaseDealImage):
             raise UnicornException("没有任何匹配的图片")
             return False, "没有任何匹配的图片"
         # 排序需要基于拍照的文件序号进行处理
-        original_photo_list.sort(
-            key=lambda x: "{}-{}-{}".format(x["goods_art_no"], x["image_index"], x["file"]))
+        # original_photo_list.sort(
+        #     key=lambda x: "{}-{}-{}".format(x["goods_art_no"], x["image_index"], x["file"]))
 
         # print(original_photo_list)
         # 对有拍摄信息的图片进行数据库比对,如有比对上,则移动至货号文件夹,否则移入历史文件夹
         total_num = len(original_photo_list)
         # 当天日期作为文件夹
         seconds = time.time()
-        output_path = "output/{f_name}".format(f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
+        output_path = "{}/{f_name}".format(settings.OUTPUT_DIR,f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
 
         # 遍历每个匹配好的数据进行处理
+        print(f"original_photo_list:",original_photo_list)
         n = 0
         for photo_dict in original_photo_list:
             n += 1
+            print("处理dict:",photo_dict)
             # 进度条
             goods_art_no = photo_dict["goods_art_no"]
             original_image_path = photo_dict["file_path"]
@@ -272,7 +325,51 @@ class DealImage(BaseDealImage):
         # 完成处理
         # self.set_state(state_value=2)
         return True, output_path
+    def dealMoveImageV2(self,goods_art_no=None) -> dict:
+        # 遍历目标文件夹,获取有拍摄信息的图片,并按拍摄时间排序
+        original_photo_list = []  # 原始图片列表
+        _data = self.get_goods_art_no_files(goods_art_no)
+        if _data:
+            for file_idx, dataItem in enumerate(_data):
+            # 能匹配上数据库
+                file_path = dataItem['image_path']
+                original_photo_list.append({"file_path": file_path,
+                                            "goods_art_no": goods_art_no,
+                                            "image_index": file_idx,
+                                            })
+        if not original_photo_list:
+            raise UnicornException("没有任何匹配的图片")
+        # 当天日期作为文件夹
+        seconds = time.time()
+        output_path = "{}/{f_name}".format(settings.OUTPUT_DIR,f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
 
+        # 遍历每个匹配好的数据进行处理
+        print(f"original_photo_list:",original_photo_list)
+        n = 0
+        for photo_dict in original_photo_list:
+            n += 1
+            print("处理dict:",photo_dict)
+            # 进度条
+            goods_art_no = photo_dict["goods_art_no"]
+            original_image_path = photo_dict["file_path"]
+            goods_art_no_path = "{output_path}/{goods_art_no}".format(output_path=output_path,
+                                                                      goods_art_no=goods_art_no)
+            # 创建货号下的一系列文件夹
+            self.create_folder(goods_art_no_path)
+            # 重命名并进行移动
+            print("开始移动:{}  {} 命名为:{}".format(goods_art_no, original_image_path, goods_art_no_path))
+            result, message = self.copy_images(goods_art_no, goods_art_no_path, original_image_path)  # 货号、货号文件路径、原始图路径
+            if not result:
+                return result, message
+            time.sleep(0.1)
+            print("已完成移动处理")
+        if n != 0:
+            # 自动生成一个货号表
+            print("output_path", output_path)
+            self.deal(output_path)
+        # 完成处理
+        # self.set_state(state_value=2)
+        return True, output_path
     def deal(cls, dir_path):
 
         # print("dir_path", dir_path)
@@ -315,14 +412,15 @@ class DealImage(BaseDealImage):
                     continue
 
                 all_original_images = os.listdir(_path)  # 遍历货号原始图文件夹
-                goods_pic_total = len(all_original_images)
-                if not all_original_images:
+                sort_result = sorted(all_original_images, key=lambda x: int(re.findall(r'((\d+))', x)[0]) if re.findall(r'((\d+))', x) else 0)
+                goods_pic_total = len(sort_result)
+                if not sort_result:
                     continue
-                image_file = all_original_images[0]  # 取第一个货号图
+                image_file = sort_result[0]  # 取第一个货号图
                 image_file_path = "{}/{}/原始图/{}".format(
                     dir_path, goods_art_no_folder, image_file
                 )
-
+                # print("\033[1;32;40m 需要处理的200images,路径: \033[0m",image_file_path,all_original_images,sort_result)
                 if not os.path.exists(
                         "{}/{}/200images".format(dir_path, goods_art_no_folder)
                 ):

+ 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

+ 18 - 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(),
                     }
@@ -209,6 +208,10 @@ class DataModeGenerateDetail(DataBaseModel):
             lambda x: x.dropna().iloc[0] if not x.dropna().empty else np.nan)
         group_local_df = group_local_df.reset_index()
 
+        # -----------统一款号列的数据类型为字符串,避免merge时报错
+        group_local_df["款号"] = group_local_df["款号"].astype(str)
+        original_df["款号"] = original_df["款号"].astype(str)
+
         # -----------数据匹配关联,左关联
         group_local_df = pd.merge(group_local_df, original_df, on=["款号"], how="left", indicator=False, )
         # -----------只取有标记的数据
@@ -286,6 +289,10 @@ class DataModeGenerateDetail(DataBaseModel):
         local_df["颜色名称"] = local_df["颜色名称"].apply(self.get_real_color_name)
         local_df.drop_duplicates(subset=['款号', "颜色名称"], keep='first', inplace=True)
 
+        # ------------统一款号列的数据类型为字符串,避免merge时报错
+        local_df["款号"] = local_df["款号"].astype(str)
+        new_df["款号"] = new_df["款号"].astype(str)
+
         # ------------excel表格关联 上述的款号 进行标记,其他删除
         _df = new_df.drop_duplicates(subset=['款号'], keep='first', inplace=False)
         _df = _df[["款号"]]

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

+ 334 - 87
python/service/grenerate_main_image_test.py

@@ -11,7 +11,8 @@ from functools import wraps
 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):
         """
@@ -350,7 +457,87 @@ class GeneratePic(object):
         time.sleep(3)
         if output_queue is not None:
             output_queue.put(True)
+    def paste_img(self,image, top_img, base="nw", value=(0, 0), ):
+            """
+            {
+                "command": "paste_img",
+                "im": 需要粘贴的图片
+                "pos": {"plugins_mode": "relative",  # pixel
+                        "base": "center",  # nw,nc,ne,ec ... 各个方向参考点
+                        "value": (100, 100),
+                        "percentage": (0.5, 0.5),
+                        },
+                "margins": (0, 0, 0, 0),  # 上下左右边距
+            }
+            """
+            value = (int(value[0]), int(value[1]))
+            # 处理默认值
+            base = "nw" if not base else base
+            top, down, left, right = 0, 0, 0, 0
+
+            # 基于右边,上下居中
+            if base == "ec" or base == "ce":
+                p_x = int(image.width - (top_img.width + value[0]))
+                p_y = int((image.height - top_img.height) / 2) + value[1]
+
+            # 基于顶部,左右居中
+            if base == "nc" or base == "cn":
+                # 顶部对齐
+                deviation_x, deviation_y = int((image.width - top_img.width) / 2), int(
+                    (image.height - top_img.height) / 2
+                )
+                p_x = deviation_x + value[0] + left
+                p_y = value[1]
+
+            # 基于右上角
+            if base == "en" or base == "ne":
+                p_x = int(image.width - (top_img.width + value[0])) + left
+                p_y = value[1]
+
+            # 基于左上角
+            if base == "nw" or base == "wn":
+                deviation_x, deviation_y = 0, 0
+                p_x, p_y = value
+
+            # 基于底部,左右居中
+            if base == "cs" or base == "sc":
+                deviation_x, deviation_y = int((image.width - top_img.width) / 2), int(
+                    (image.height - top_img.height) / 2
+                )
+
+                p_y = image.height - (top_img.height + value[1] + down)
+                p_x = deviation_x + value[0] + left
+
+            # 上下左右居中
+            if base == "center" or base == "cc":
+                deviation_x, deviation_y = int((image.width - top_img.width) / 2), int(
+                    (image.height - top_img.height) / 2
+                )
+                p_x = deviation_x + value[0] + left
+                p_y = deviation_y + value[1] + top
+
+            # 基于左下角
+            if base == "sw" or base == "ws":
+                # deviation_x, deviation_y = 0, int((img.height - img_1.height))
+                p_x = value[0] + left
+                p_y = image.height - (top_img.height + value[1] + down)
+
+            # 基于左边,上下居中
+            if base == "wc" or base == "cw":
+                p_x = value[0] + left
+                p_y = int((image.height - top_img.height) / 2) + value[1] + top
+
+            # 基于右下角
+            if base == "es" or base == "se":
+                p_x = int(image.width - (top_img.width + value[0])) + left
+                p_y = image.height - (top_img.height + value[1] + down) + top
+
+            try:
+                image.paste(top_img, box=(p_x, p_y), mask=top_img)
+            except:
+                image.paste(top_img, box=(p_x, p_y), mask=top_img.convert("RGBA"))
 
+            return image
     @time_it
     def run(
         self,
@@ -386,23 +573,41 @@ class GeneratePic(object):
             output_queue = kwargs["output_queue"]
         else:
             output_queue = None
-
+        # image_deal_mode = 0#不翻转图像
+        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()
-        orign_im = Image.open(image_path)  # 原始图
+        with Image.open(image_path) as orign_im:
+            # 复制图像以便后续操作
+            orign_im = orign_im.copy()
         print("242  need_time_1:{}".format(time.time() - _s))
-
         orign_x, orign_y = orign_im.size
-        cut_image = Image.open(cut_image_path)  # 原始图的已扣图
+        with Image.open(cut_image_path) as cut_image:
+            # 复制图像以便后续操作
+            cut_image = cut_image.copy()
         cut_image, new_box = get_mini_crop_img(img=cut_image)
         im_shadow = orign_im.crop(new_box)  # 切图
         new_x, new_y = im_shadow.size
 
         # ================自动色阶处理
         _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)
@@ -430,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()
@@ -462,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)
@@ -474,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)
@@ -482,70 +699,85 @@ class GeneratePic(object):
         # 不生成主图时直接退出
         if not out_path:
             return True
-
-        # im_shadow.show()
-        # =====================主图物体的缩放依据大小
-        if max_box:
-            im_shadow = to_resize(_im=im_shadow, width=max_box[0], high=max_box[1])
-            cut_image = to_resize(_im=cut_image, width=max_box[0], high=max_box[1])
-        else:
-            if resize_mode is None:
-                im_shadow = to_resize(_im=im_shadow, width=1400, high=1400)
-                cut_image = to_resize(_im=cut_image, width=1400, high=1400)
-
-            elif resize_mode == 1:
-                im_shadow = to_resize(_im=im_shadow, width=1400, high=1400)
-                cut_image = to_resize(_im=cut_image, width=1400, high=1400)
-
-            elif resize_mode == 2:
-                # todo 兼容长筒靴等,将图片大小限制在一个指定的box内
-                im_shadow = to_resize(_im=im_shadow, width=650)
-                cut_image = to_resize(_im=cut_image, width=650)
-                # 再次检查需要约束缩小到一定高度,适应长筒靴
-                _im_x, _im_y = cut_image.size
-                if _im_y > 1400:
-                    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)
-
         if image_deal_mode == 1:
             # 翻转
             im_shadow = im_shadow.transpose(Image.FLIP_LEFT_RIGHT)
             cut_image = cut_image.transpose(Image.FLIP_LEFT_RIGHT)
+        image_margin = int(padding_800image)
+        bg_size = (1600, 1600)
+        _offset_x, _offset_y = 0, 0
+        scale_rate = 1
+        # im_shadow.show()
+        # =====================主图物体的缩放依据大小
+        if image_margin is not None:
+            _bbox = cut_image.getbbox()
+            _x, _y = _bbox[0], _bbox[1]
+            _w, _h = _bbox[2] - _bbox[0], _bbox[3] - _bbox[1]
+            # 中心偏移量
+            offset_x, offset_y = _x - (cut_image.width - _w) / 2, _y - (cut_image.height - _h) / 2,
+            # print("中心偏移量:", offset_x, offset_y)
+            # 透明底最小矩形
+            scale_rate = self.get_scale(base_by_box=(bg_size[0] - image_margin * 2, bg_size[1] - image_margin * 2), image_size=(_w, _h))
+            # 计算缩放比例,以及顶点相对位置
+            # print("缩放比例:", scale_rate)
+            # 偏移量
+            _offset_x, _offset_y = offset_x * scale_rate, offset_y * scale_rate
+            # print("偏移量:", _offset_x, _offset_y)
+            # 阴影图缩放尺寸
+            cut_image = to_resize(_im=cut_image, width=cut_image.width * scale_rate)
+            im_shadow = to_resize(_im=im_shadow, width=im_shadow.width * scale_rate)
 
-        # 创建底层背景
-        image_bg = Image.new("RGB", (1600, 1600), (255, 255, 255))
+        else:
+            if max_box:
+                im_shadow = to_resize(_im=im_shadow, width=max_box[0], high=max_box[1])
+                cut_image = to_resize(_im=cut_image, width=max_box[0], high=max_box[1])
+            else:
+                size_defind = 1400
+                if resize_mode is None:
+                    im_shadow = to_resize(_im=im_shadow, width=size_defind, high=size_defind)
+                    cut_image = to_resize(_im=cut_image, width=size_defind, high=size_defind)
+
+                elif resize_mode == 1:
+                    im_shadow = to_resize(_im=im_shadow, width=size_defind, high=size_defind)
+                    cut_image = to_resize(_im=cut_image, width=size_defind, high=size_defind)
+
+                elif resize_mode == 2:
+                    # todo 兼容长筒靴等,将图片大小限制在一个指定的box内
+                    im_shadow = to_resize(_im=im_shadow, width=650)
+                    cut_image = to_resize(_im=cut_image, width=650)
+                    # 再次检查需要约束缩小到一定高度,适应长筒靴
+                    _im_x, _im_y = cut_image.size
+                    if _im_y > 1400:
+                        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, 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:
@@ -566,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)
@@ -583,30 +814,29 @@ class GeneratePic(object):
                 file_without_suffix = out_path
                 suffix = ""
             # 单独拼接字符串示例
-            new_file_path = f"{file_without_suffix}_{imageSize}.{suffix}"
-            if imageSize < 1600:
-                image_bg = image_bg.resize(
-                    (imageSize, imageSize), resample=settings.RESIZE_IMAGE_MODE
+            image_size_int = int(imageSize)
+            image_size_str = str(imageSize)
+            new_file_path = f"{file_without_suffix}_{image_size_str}.{suffix}"
+            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]
@@ -615,21 +845,38 @@ 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)
-
-        if output_queue is not None:
-            output_queue.put(True)
+        # 在函数结束时使用更安全的关闭方式
+        # 清理所有可能打开的图片对象
+        for img_var in ['orign_im', 'cut_image', 'logo_im', 'out_image_1', 'out_image_2']:
+            if img_var in locals():
+                img = locals()[img_var]
+                if hasattr(img, 'close'):
+                    try:
+                        img.close()
+                    except Exception as e:
+                        logger.warning(f"关闭图片对象 {img_var} 时出错: {e}")
+            if output_queue is not None:
+                output_queue.put(True)
         return True
+    def get_scale(self,base_by_box, image_size):
+        box_width, box_height = int(base_by_box[0]), int(base_by_box[1])
+        width, height = image_size[0], image_size[1]
+        if box_width / box_height < width / height:
+            scale = box_width / width
+        else:
+            scale = box_height / height
+        return scale

+ 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))

+ 2 - 0
python/service/image_pic_deal.py

@@ -32,6 +32,7 @@ class OnePicDeal(object):
             print("自动识别----->这是左脚")
         else:
             print("自动识别----->这是右脚")
+        im.close()
         return flag
 
     def check_shoe_is_right_by_pixel(self, im=None, image_path=None):
@@ -57,6 +58,7 @@ class OnePicDeal(object):
                 left_f_num += 1
             else:
                 left_f_num -= 1
+        im.close()
         if left_f_num > 0:
             return True
         else:

+ 44 - 4
python/service/match_and_cutout_mode_control/base_deal_image_v2.py

@@ -181,8 +181,45 @@ class BaseDealImage(object):
             return {'code': 1, 'msg': '图片位置与顺序重复,请检查您的输入'}
 
         for val in imageOrderList:
-            if val not in ["俯视", "侧视", "后跟", "鞋底", "内里", "组合", "组合2", "组合3", "组合4", "组合5"]:
-                return {'code': 1, 'msg': '可选项为:俯视,侧视,后跟,鞋底,内里,组合,组合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": f"可选项为:{image_orders_str}",
+                }
 
         if resize_image_view not in imageOrderList:
             return {'code': 1, 'msg': '缩小的步骤必须是你填写的图片顺序中'}
@@ -257,8 +294,11 @@ class BaseDealImage(object):
         is_image_deal_mode = 0
 
         # 删除目录再新建
-        if os.path.exists('{}/阴影图处理'.format(folder_path)):
+        try:
+          if os.path.exists('{}/阴影图处理'.format(folder_path)):
             shutil.rmtree('{}/阴影图处理'.format(folder_path))
+        except:
+          print('An exception occurred')
 
         self.crate_all_folders(folder_path)
 
@@ -972,7 +1012,7 @@ class BaseDealImage(object):
         total_num = len(original_photo_list)
         # 当天日期作为文件夹
         seconds = time.time()
-        output_path = "output/{f_name}".format(f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
+        output_path = "{output}/{f_name}".format(output=settings.OUTPUT_DIR,f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
 
         # 遍历每个匹配好的数据进行处理
         n = 0

+ 1 - 1
python/service/match_and_cutout_mode_control/module_matching_photos_v2.py

@@ -904,7 +904,7 @@ class MatchingPhotos(MineQWidget):
         total_num = len(original_photo_list)
         # 当天日期作为文件夹
         seconds = time.time()
-        output_path = "output/{f_name}".format(
+        output_path = "{output}/{f_name}".format(output=settings.OUTPUT_DIR,
             f_name=time.strftime("%Y-%m-%d", time.localtime(seconds))
         )
 

+ 1 - 1
python/service/matching_photos/module_matching_photos.py

@@ -553,7 +553,7 @@ class MatchingPhotos(MineQWidget):
         total_num = len(original_photo_list)
         # 当天日期作为文件夹
         seconds = time.time()
-        output_path = "output/{f_name}".format(
+        output_path = "{output}/{f_name}".format(output=settings.OUTPUT_DIR,
             f_name=time.strftime("%Y-%m-%d", time.localtime(seconds))
         )
 

+ 10 - 1
python/service/multi_threaded_image_saving.py

@@ -77,16 +77,25 @@ 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))
+                    _format = "JPEG" if _format.upper()=="JPG" else _format
+                    print("保存图片[format]:{}".format(_format))
                     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()
 
     def get_completed_images(self, file_path):
         """

+ 136 - 45
python/service/online_request/module_online_data.py

@@ -7,7 +7,9 @@ from utils.common import message_queue
 from middleware import UnicornException
 from PIL import Image
 import io, time
+from logger import logger
 
+# logger = logging.getLogger(__name__)
 
 class JsonEncoder(json.JSONEncoder):
     """Convert numpy classes to JSON serializable objects."""
@@ -54,7 +56,7 @@ class AIGCDataRequest(object):
         resultData = self.s.post(
             url, files={"file": open(local_path, "rb")}, headers=post_headers
         ).json()
-        
+
         return resultData["data"]["url"]
 
     def center_paste_image(
@@ -110,7 +112,7 @@ class AIGCDataRequest(object):
             # 如果保存为JPG等不支持透明度的格式,转换为RGB并使用白色背景
             background = background.convert("RGB")
             background.save(output_path)
-
+        source_img.close()
         return background
 
     def generateProductScene(self, local_path, prompt, save_path):
@@ -124,41 +126,102 @@ class AIGCDataRequest(object):
             "gemini_model": "gemini-2.5-flash-image-preview",
         }
         """生成场景图"""
-        url = settings.DOMAIN + "/api/ai_image/inspired/command_to_image"
+        try:
+            url = settings.DOMAIN + "/api/ai_image/inspired/command_to_image"
+            resultData = self.s.post(url, data=data, headers=self.post_headers, timeout=80).json()
+
+            code = resultData.get("code", 0)
+            message = resultData.get("message", "")
+            if code != 0:
+                raise UnicornException(message)
+            image_arr = resultData.get("data", None).get("image", [])
+            if len(image_arr) == 0:
+                raise UnicornException("场景图生成失败")
+            image_url = image_arr[0]
+            save_image_path = download_image_with_pil(image_url, save_path)
+            return save_image_path
+        except:
+          raise UnicornException("场景图生成失败")
+
+    def generateProductSceneQW(self, local_path, prompt, save_path):
+        '''千问生成场景图'''
+        imageUrl = self.uploadImage(local_path)
+        data = {
+            "machine_type": 0,  # 0鞋;1服装
+            "generate_type": 0,  # 生成类型,这里指代得是场景图还是模特图;0场景图;1模特图
+            "base_image": imageUrl,
+            "prompt": prompt
+        }
+        """生成场景图"""
+        url = settings.DOMAIN + "/api/ai_image/main/image_edit_generate"
         resultData = self.s.post(url, data=data, headers=self.post_headers).json()
-        
+
         code = resultData.get("code", 0)
         message = resultData.get("message", "")
         if code != 0:
             raise UnicornException(message)
-        image_arr = resultData.get("data", None).get("image", [])
-        if len(image_arr) == 0:
+        image_url = resultData.get("data", None).get("image_url", '')
+        if image_url == "" or image_url is None:
             raise UnicornException("场景图生成失败")
-        image_url = image_arr[0]
         save_image_path = download_image_with_pil(image_url, save_path)
         return save_image_path
 
-    def searchProgress(self, id):
-        """查询进度"""
-        url = settings.DOMAIN + "/api/ai_image/main/search_bacth_progress"
-        data = {"site": 1, "generate_ids": [id], "type": "aigc_pro"}
-        resultData = self.s.post(url, json=data, headers=self.post_headers)
-        resultData = resultData.json()
-        
+    def generateModelShoesQW(self, local_path, model_id, save_path):
+        '''千问生成场景图'''
+        imageUrl = self.uploadImage(local_path)
+        data = {
+            "machine_type": 0,  # 0鞋;1服装
+            "generate_type": 1,  # 生成类型,这里指代得是场景图还是模特图;0场景图;1模特图
+            "base_image": imageUrl,
+            "model_template_id": model_id,
+        }
+        """生成场景图"""
+        url = settings.DOMAIN + "/api/ai_image/main/image_edit_generate"
+        resultData = self.s.post(url, data=data, headers=self.post_headers).json()
+
         code = resultData.get("code", 0)
         message = resultData.get("message", "")
         if code != 0:
             raise UnicornException(message)
-        data_result = resultData.get("data", [])
-        if len(data_result) == 0:
+        image_url = resultData.get("data", None).get("image_url", '')
+        if image_url == "" or image_url is None:
+            raise UnicornException("模特图生成失败")
+        save_image_path = download_image_with_pil(image_url, save_path)
+        return save_image_path
+
+    def searchProgress(self, id):
+        """查询进度"""
+        try:
+            url = settings.DOMAIN + "/api/ai_image/main/search_bacth_progress"
+            data = {"site": 1, "generate_ids": [id], "type": "aigc_pro"}
+            resultData = self.s.post(url, json=data, headers=self.post_headers, timeout=10)
+            resultData = resultData.json()
+            code = resultData.get("code", 0)
+            message = resultData.get("message", "")
+            if code != 0:
+                raise UnicornException(message)
+            data_result = resultData.get("data", [])
+            print("查询进度", data_result)
+            logger.info(f"查询进度:{data_result}")
+            if len(data_result) == 0:
+                return -1, None
+            data_item = data_result[0]
+            status = data_item.get("status", -1)
+            result_image = None
+            
+            # 只有在完成状态下才返回图片URL
+            if status == 2:  # 完成
+                result_image_urls = data_item.get("result_image_urls", [])
+                result_image_urls = [] if result_image_urls is None else result_image_urls
+                result_image = result_image_urls[0] if len(result_image_urls) > 0 else None
+                
+            return status, result_image
+        except requests.Timeout:
+            print("查询进度超时")
+            return -1, None
+        except Exception as e:
+            print(f"查询进度异常: {e}")
             return -1, None
-        data_item = data_result[0]
-        status = data_item.get("status", -1)
-        if status in [0,1]:
-            return status, None
-        result_image_urls = data_item.get("result_image_urls", [])
-        result_image = result_image_urls[0] if len(result_image_urls) > 0 else None
-        return status, result_image
 
     def generateUpperShoes(self, local_path, model_id, save_path):
         """生成上脚图"""
@@ -175,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:
@@ -184,18 +247,24 @@ class AIGCDataRequest(object):
         if len(generate_ids) == 0:
             raise UnicornException("模特图生成失败")
         generate_id = generate_ids[0]
-        search_times = 60
+        search_times = 80
         status = 0
         result_image = None
         print("generate_id", generate_id)
         while search_times > 0:
-            print(f"查询第{search_times}次")
+            print(f"模特图查询第{search_times}次")
+            logger.info(f"模特图查询第{search_times}次")
             status, result_image = self.searchProgress(generate_id)
-            if status in [-1, 2]:
+            # status: -1=失败, 0=排队中, 1=进行中, 2=完成
+            if status == 2:  # 完成
                 break
+            if status == -1:  # 失败
+                break
+            # status为0(排队中)或1(进行中)时继续查询
             time.sleep(1)
             search_times -= 1
-        if not result_image:
+        # 循环结束后检查最终状态
+        if status == -1 or (status != 2 and search_times <= 0):
             raise UnicornException("模特图生成失败")
         save_image_path = download_image_with_pil(result_image, save_path)
         print("上脚图save_image_path",result_image, save_image_path)
@@ -357,7 +426,7 @@ class OnlineDataRequest(object):
         resultData = self.s.post(
             url, files={"file": open(local_path, "rb")}, headers=post_headers
         ).json()
-        
+
         return resultData["data"]["url"]
 
     def get_current_menu(self):
@@ -514,7 +583,7 @@ class OnlineDataRequest(object):
         # print("上传商品api==>url", url)
         # print("上传第三方数据打印", params)
         resultData = self.s.post(url, data=postData, headers=post_headers).json()
-        
+
         print("上传商品api==>resultData", resultData)
         return resultData
 
@@ -558,21 +627,30 @@ 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)
+                    # 尺码
                     mainImages = sku_data.get("800x800", [])
                     if not mainImages:
                         continue
                     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),
                         "skuMainImageUrl": str(imageUrl),
-                        "skuName": f"颜色:{color_name}",
+                        "skuName": f"颜色:{color_name};尺寸:{size}",
                         "sellingPrice": float(goods_price),
                         "quantity": int(quantity),
                         "showOrder": int(skuIdx + 1),
+                        "skuNameJson":skuNameJson
                     }
                     skuList.append(skuItemData)
                     itemImage = {
@@ -589,7 +667,7 @@ class OnlineDataRequest(object):
                     }
                     skuPropValueList.append(
                         {
-                            "imageJson": imageJson,
+                            "imageJson": [imageJson],
                             "propValue": str(color_name),
                             "showOrder": 1,
                         }
@@ -602,6 +680,17 @@ class OnlineDataRequest(object):
                         "skuPropValueList": skuPropValueList,
                     }
                 )
+                itemSkuImageList.append({
+                        "propName": "尺寸",
+                        "isImageProp": 0,
+                        "propShowOrder": 1,
+                        "showOrder": 0,
+                        "propValue": str(size),
+                        "skuPropValueList":[{
+                            "propValue": str(size),
+                            "showOrder": 1,
+                            }]
+                    })
                 detailImageUrl = self.uploadImage(local_path=detail_path)
                 category_info = "流行男鞋>>休闲鞋>>时尚休闲鞋"
                 itemData = {
@@ -719,23 +808,25 @@ class GetOnlineDataHLM(OnlineDataRequest):
             return {}
 
         for data in response_data["data"]:
-            goods_number_data[data["goods_art_no"]] = {}
-            goods_number_data[data["goods_art_no"]]["商品货号"] = data[
+            goods_art_no = str(data["goods_art_no"])
+            goods_number_data[goods_art_no] = {}
+            goods_number_data[goods_art_no]["商品货号"] = data[
                 "goods_art_no"
             ].upper()
-            goods_number_data[data["goods_art_no"]]["款号"] = data[
+            goods_number_data[goods_art_no]["款号"] = data[
                 "goods_number"
             ].upper()
-            goods_number_data[data["goods_art_no"]]["商品面料"] = data["fabric"]
-            goods_number_data[data["goods_art_no"]]["商品内里"] = data["lining"]
-            goods_number_data[data["goods_art_no"]]["商品鞋底"] = data["sole"]
-            goods_number_data[data["goods_art_no"]]["鞋垫"] = data["insole"]
-            goods_number_data[data["goods_art_no"]]["颜色名称"] = data["color"]
-            goods_number_data[data["goods_art_no"]]["商品标题"] = data["goods_title"]
-            goods_number_data[data["goods_art_no"]]["商品价格"] = data["retail_price"]
-            goods_number_data[data["goods_art_no"]]["性别"] = data["gender"]
-            goods_number_data[data["goods_art_no"]]["token"] = self.token
-
+            goods_number_data[goods_art_no]["商品面料"] = data["fabric"]
+            goods_number_data[goods_art_no]["商品内里"] = data["lining"]
+            goods_number_data[goods_art_no]["商品鞋底"] = data["sole"]
+            goods_number_data[goods_art_no]["鞋垫"] = data["insole"]
+            goods_number_data[goods_art_no]["颜色名称"] = data["color"]
+            goods_number_data[goods_art_no]["商品标题"] = data["goods_title"]
+            goods_number_data[goods_art_no]["商品价格"] = data["retail_price"]
+            goods_number_data[goods_art_no]["尺码"] = data["size"]
+            goods_number_data[goods_art_no]["性别"] = data["gender"]
+            goods_number_data[goods_art_no]["token"] = self.token
+        print("货号数据:", goods_number_data)
         return goods_number_data
 
     def uploadImage(self, local_path: str) -> str:

+ 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:

+ 411 - 300
python/service/run_main.py

@@ -15,14 +15,14 @@ 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
 from middleware import UnicornException
 from settings import recordDataPoint
 import asyncio
-
+from utils.common import message_queue
 
 class RunMain:
     # run_end_sign = Signal(dict)
@@ -35,6 +35,9 @@ class RunMain:
     # dialog_result_signal = Signal(str)
 
     # dialog_result_signal = Signal(str)
+    
+    # 重试机制配置:最大重试次数(固定为1,避免死循环)
+    MAX_RETRY_COUNT = 1
 
     def __init__(self, windows, token, uuid):
         super().__init__()
@@ -186,22 +189,10 @@ class RunMain:
 
     # 抠图校验后的回调函数处理
     def check_for_cutout_image_first_call_back(self, return_data):
-        # return_data = {
-        #     "code": 99,
-        #     "message": "",
-        #     "data": {
-        #         "all_goods_art_no_folder_data": [],
-        #     },
-        # }
         code = return_data["code"]
         config_data = return_data["data"]["config_data"]
         config_data["sign_text"] = ""
         if code != 0:
-            # self.windows.show_message(return_data["message"])
-            # self.show_progress_detail(return_data["message"])
-            # _dialog_dict = {"text": return_data["message"],
-            #                 "windows": self,
-            #                 }
             raise UnicornException(return_data["message"])
 
         do_next = False
@@ -233,27 +224,6 @@ class RunMain:
         elif button_1 == "继续" and button_2 is None and button_3 is None:
             do_next = True
         else:
-            # print("runmain  179----------------")
-            # # print(self)
-            # # print(self.windows)
-            # _dialog_dict = {"text": text,
-            #                 "button_1": button_1,
-            #                 "button_2": button_2,
-            #                 "button_3": button_3,
-            #                 "windows": self,
-            #                 }
-            # self.show_dialog_sign.emit(_dialog_dict)
-            # # self.exec_()
-            # # 等待事件被设置
-            # self.event.wait()
-            # print("self.dialog_result", self.dialog_result)
-            # #
-            # # my_dialog = DialogShow(self.windows, text=text, button_1=button_1, button_2=button_2,
-            # #                        button_3=button_3)
-            # # ret = my_dialog.exec()
-            # print("460  ===============my_dialog.flag_name===============")
-            # print(my_dialog.flag_name)
-
             if "移除" in self.dialog_result:
                 for error_folder_data in [
                     x for x in all_goods_art_no_folder_data if x["label"] == "错误"
@@ -270,28 +240,6 @@ class RunMain:
             goods_art_no_folder_data["folder_name"]
             for goods_art_no_folder_data in all_goods_art_no_folder_data
         ]
-        # try:
-        #     loop = asyncio.get_event_loop()
-        #     loop.create_task(sendSocketMessage(
-        #         code=0,
-        #         msg="开始处理抠图",
-        #         data={
-        #             "status": "进行中",
-        #             "goods_art_nos": goods_arts,
-        #         },
-        #         msg_type="segment_progress",
-        #     ))
-        # except:
-        #     print('An exception occurred')
-        #     asyncio.run(sendSocketMessage(
-        #         code=0,
-        #         msg="开始处理抠图",
-        #         data={
-        #             "status": "进行中",
-        #             "goods_art_nos": goods_arts,
-        #         },
-        #         msg_type="segment_progress",
-        #     ))
         if do_next:
             all_goods_art_no_folder_data = [
                 x for x in all_goods_art_no_folder_data if x["label"] == "待处理"
@@ -312,34 +260,172 @@ class RunMain:
             return new_func
         else:
             print("已结束抠图处理")
-            # try:
-            #     loop = asyncio.get_event_loop()
-            #     loop.create_task(
-            #         sendSocketMessage(
-            #             code=0,
-            #             msg="抠图结束",
-            #             data={
-            #                 "status": "已完成",
-            #                 "goods_art_nos": goods_arts,
-            #             },
-            #             msg_type="segment_progress",
-            #         )
-            #     )
-            # except:
-            #     print('An exception occurred')
-            #     asyncio.run(
-            #         sendSocketMessage(
-            #             code=0,
-            #             msg="抠图结束",
-            #             data={
-            #                 "status": "已完成",
-            #                 "goods_art_nos": goods_arts,
-            #             },
-            #             msg_type="segment_progress",
-            #         )
-            #     )
             return True
 
+    def validate_folder_integrity(self, folder_path, folder_name, expected_output_count=None):
+        """
+        验证货号文件夹的完整性
+        检查800x800目录和阴影图处理目录中的文件数量是否等于预期数量
+        
+        Args:
+            folder_path: 货号文件夹路径
+            folder_name: 货号名称
+            expected_output_count: 预期的输出文件数量(如果为None则根据原始图数量计算)
+            
+        Returns:
+            tuple: (is_valid, original_count, processed_count, expected_count, message)
+        """
+        from logger import logger
+        import settings
+        
+        original_dir = "{}/原始图".format(folder_path)
+        processed_dir = "{}/800x800".format(folder_path)
+        shadow_dir = "{}/阴影图处理".format(folder_path)
+        
+        # 检查目录是否存在
+        if not os.path.exists(original_dir):
+            logger.warning(f"[目录校验] 货号 {folder_name} - 原始图目录不存在: {original_dir}")
+            return False, 0, 0, 0, f"原始图目录不存在"
+        
+        if not os.path.exists(processed_dir):
+            logger.warning(f"[目录校验] 货号 {folder_name} - 800x800目录不存在: {processed_dir}")
+            return False, 0, 0, 0, f"800x800目录不存在"
+        
+        if not os.path.exists(shadow_dir):
+            logger.warning(f"[目录校验] 货号 {folder_name} - 阴影图处理目录不存在: {shadow_dir}")
+            return False, 0, 0, 0, f"阴影图处理目录不存在"
+        
+        # 统计原始图文件数量(只统计图片文件)
+        _Type = [".png", ".PNG", ".jpg", ".JPG", ".gif", ".GIF", ".jpge", ".JPGE"]
+        original_files = []
+        for f in os.listdir(original_dir):
+            _, ext = os.path.splitext(f)
+            if ext in _Type:
+                original_files.append(f)
+        
+        original_count = len(original_files)
+        
+        # 统计800x800目录文件数量
+        processed_files = []
+        for f in os.listdir(processed_dir):
+            _, ext = os.path.splitext(f)
+            if ext in _Type:
+                processed_files.append(f)
+        
+        processed_count = len(processed_files)
+        
+        # 统计阴影图处理目录文件数量
+        shadow_files = []
+        for f in os.listdir(shadow_dir):
+            _, ext = os.path.splitext(f)
+            if ext in _Type:
+                shadow_files.append(f)
+        
+        shadow_count = len(shadow_files)
+        
+        # 计算预期的输出文件数量
+        if expected_output_count is None:
+            # 从配置中获取主图尺寸列表
+            try:
+                out_pic_size_list = settings.getSysConfigs("basic_configs", "main_image_size", [1600])
+                # 如果是字符串,尝试解析为列表
+                if isinstance(out_pic_size_list, str):
+                    import json
+                    try:
+                        out_pic_size_list = json.loads(out_pic_size_list)
+                    except:
+                        out_pic_size_list = [1600]
+                # 确保是列表
+                if not isinstance(out_pic_size_list, list):
+                    out_pic_size_list = [out_pic_size_list]
+                # 过滤空值
+                out_pic_size_list = [x for x in out_pic_size_list if x]
+                
+                if not out_pic_size_list:
+                    out_pic_size_list = [1600]
+                
+                # 预期输出数量 = 原始图数量 * 尺寸数量
+                expected_count = original_count * len(out_pic_size_list)
+                logger.info(f"[目录校验] 货号 {folder_name} - 配置的输出尺寸: {out_pic_size_list}, 共{len(out_pic_size_list)}个尺寸")
+            except Exception as e:
+                logger.warning(f"[目录校验] 货号 {folder_name} - 获取配置失败,使用默认值: {e}")
+                expected_count = original_count  # 默认1倍
+        else:
+            expected_count = expected_output_count
+        
+        # 预期的阴影图数量 = 原始图数量 * 2(每个原图生成2个文件:阴影图和抠图)
+        expected_shadow_count = original_count * 2
+        
+        logger.info(f"[目录校验] 货号 {folder_name} - 原始图: {original_count}张, 800x800: {processed_count}张, 预期: {expected_count}张, 阴影图: {shadow_count}张, 预期阴影图: {expected_shadow_count}张")
+        
+        # 如果原始图为空,认为无效
+        if original_count == 0:
+            logger.error(f"[目录校验] 货号 {folder_name} - 原始图目录为空")
+            return False, original_count, processed_count, expected_count, "原始图目录为空"
+        
+        # 严格检查:800x800文件数量必须等于预期数量
+        if processed_count != expected_count:
+            logger.error(f"[目录校验] 货号 {folder_name} - 800x800处理失败: 预期{expected_count}张,实际{processed_count}张 (原始图{original_count}张 × {len(out_pic_size_list) if 'out_pic_size_list' in locals() else '?'}个尺寸)")
+            return False, original_count, processed_count, expected_count, f"800x800文件数量不匹配: 预期{expected_count}张,实际{processed_count}张"
+        
+        # 严格检查:阴影图处理文件数量必须等于原始图的2倍
+        if shadow_count != expected_shadow_count:
+            logger.error(f"[目录校验] 货号 {folder_name} - 阴影图处理失败: 预期{expected_shadow_count}张(原始图{original_count}张 × 2),实际{shadow_count}张")
+            return False, original_count, processed_count, expected_count, f"阴影图文件数量不匹配: 预期{expected_shadow_count}张,实际{shadow_count}张"
+        
+        logger.info(f"[目录校验] 货号 {folder_name} - 校验通过 ✓")
+        return True, original_count, processed_count, expected_count, "校验通过"
+    
+    def retry_single_folder(self, goods_art_no_folder_data, image_order_list, cutout_mode, resize_image_view, logo_path, callback_func):
+        """
+        重试单个货号的处理(仅执行一次,不会递归或循环)
+        
+        Args:
+            goods_art_no_folder_data: 货号文件夹数据
+            image_order_list: 图片顺序列表
+            cutout_mode: 抠图模式
+            resize_image_view: 缩放视角
+            logo_path: Logo路径
+            callback_func: 回调函数
+            
+        Returns:
+            bool: 重试是否成功
+        """
+        from logger import logger
+        
+        folder_name = goods_art_no_folder_data["folder_name"]
+        folder_path = goods_art_no_folder_data["folder_path"]
+        
+        logger.info(f"[重试处理] 开始重试货号: {folder_name} (仅此一次,不会重复重试)")
+        callback_func(f"正在重试货号: {folder_name}")
+        
+        deal = BaseDealImage(token=self.token)
+        try:
+            # 只处理这一个货号 - 注意:这里直接调用处理逻辑,不会再触发校验和重试
+            result = deal.shoes_run_one_folder_to_deal(
+                goods_art_no_folder_data=goods_art_no_folder_data,
+                resize_image_view=resize_image_view,
+                logo_path=logo_path,
+                image_order_list=image_order_list,
+                callback_func=callback_func,
+                windows=None,
+            )
+
+            if result:
+                logger.info(f"[重试处理] 货号 {folder_name} 重试成功 ✓")
+                callback_func(f"货号 {folder_name} 重试成功")
+                return True
+            else:
+                logger.error(f"[重试处理] 货号 {folder_name} 重试失败 (不再重试)")
+                callback_func(f"货号 {folder_name} 重试失败")
+                return False
+        except Exception as e:
+            import traceback
+            logger.error(f"[重试处理] 货号 {folder_name} 重试异常: {e}\n{traceback.format_exc()}")
+            callback_func(f"货号 {folder_name} 重试异常: {e}")
+            raise
+            return False
+
     def do_run_cutout_image(
         self,
         all_goods_art_no_folder_data,
@@ -351,6 +437,7 @@ class RunMain:
         logo_path,
         config_data,
     ):
+        from logger import logger
         try:
             loop = asyncio.get_event_loop()
         except:
@@ -362,32 +449,83 @@ class RunMain:
         ]
         print("BaseDealImage().run_main========>>>>")
         deal = BaseDealImage(token=self.token)
-        try:
-            loop.run_in_executor(
-                executor,
-                deal.run_main(
-                    all_goods_art_no_folder_data=all_goods_art_no_folder_data,
-                    callback_func=callback_func,
+        deal.run_main(
+            all_goods_art_no_folder_data=all_goods_art_no_folder_data,
+            callback_func=callback_func,
+            image_order_list=image_order_list,
+            cutout_mode=cutout_mode,
+            resize_image_view=resize_image_view,
+            windows=windows,
+            logo_path=logo_path,
+        )
+
+        # ========== 后置校验:检查所有货号的目录完整性 ==========
+        logger.info("=" * 50)
+        logger.info("[后置校验] 开始检查所有货号的目录完整性")
+        logger.info("=" * 50)
+        
+        failed_folders = []
+        for goods_art_no_folder_data in all_goods_art_no_folder_data:
+            if goods_art_no_folder_data["label"] != "待处理":
+                continue
+            
+            folder_name = goods_art_no_folder_data["folder_name"]
+            folder_path = goods_art_no_folder_data["folder_path"]
+            
+            is_valid, original_count, processed_count, expected_count, message = self.validate_folder_integrity(
+                folder_path, folder_name
+            )
+            
+            if not is_valid:
+                logger.warning(f"[后置校验] 货号 {folder_name} 校验失败: {message}")
+                failed_folders.append({
+                    "data": goods_art_no_folder_data,
+                    "reason": message,
+                    "original_count": original_count,
+                    "processed_count": processed_count,
+                    "expected_count": expected_count
+                })
+        
+        # ========== 如果有失败的货号,进行重试(仅重试一次,避免死循环)==========
+        if failed_folders:
+            logger.info("=" * 50)
+            logger.info(f"[重试机制] 发现 {len(failed_folders)} 个货号需要重试")
+            logger.info(f"[重试机制] 重要提示:每个货号最多重试 {self.MAX_RETRY_COUNT} 次,不会无限重试")
+            logger.info("=" * 50)
+            
+            retry_success_count = 0
+            retry_failed_count = 0
+            
+            for failed_item in failed_folders:
+                folder_name = failed_item["data"]["folder_name"]
+                logger.info(f"[重试机制] 开始重试货号: {folder_name}, 原因: {failed_item['reason']}")
+                callback_func(f"检测到 {folder_name} 处理不完整,正在重试...")
+                
+                # 执行单次重试(retry_single_folder 内部不会再触发校验和重试)
+                # 注意:这里只调用一次,不会循环或递归
+                retry_result = self.retry_single_folder(
+                    goods_art_no_folder_data=failed_item["data"],
                     image_order_list=image_order_list,
                     cutout_mode=cutout_mode,
                     resize_image_view=resize_image_view,
-                    windows=windows,
                     logo_path=logo_path,
-                ),
-            )
-        except UnicornException as e:
-            raise UnicornException(e.msg)
-        except Exception as e:
-            raise UnicornException(e)
-        # deal.run_main(
-        #     all_goods_art_no_folder_data=all_goods_art_no_folder_data,
-        #     callback_func=callback_func,
-        #     image_order_list=image_order_list,
-        #     cutout_mode=cutout_mode,
-        #     resize_image_view=resize_image_view,
-        #     windows=windows,
-        #     logo_path=logo_path,
-        # )
+                    callback_func=callback_func
+                )
+                
+                if retry_result:
+                    retry_success_count += 1
+                else:
+                    retry_failed_count += 1
+                    logger.warning(f"[重试机制] 货号 {folder_name} 重试后仍然失败,不再继续重试(已达最大重试次数 {self.MAX_RETRY_COUNT})")
+            
+            logger.info("=" * 50)
+            logger.info(f"[重试机制] 重试完成: 成功 {retry_success_count} 个, 失败 {retry_failed_count} 个")
+            logger.info(f"[重试机制] 所有货号处理结束(包含一次重试),流程终止")
+            logger.info("=" * 50)
+            
+            callback_func(f"重试完成: 成功 {retry_success_count} 个, 失败 {retry_failed_count} 个")
+        else:
+            logger.info("[后置校验] 所有货号校验通过,无需重试 ✓")
 
         recordDataPoint(
             token=self.token,
@@ -395,31 +533,6 @@ class RunMain:
             page="抠图结束",
             data=goods_arts,
         )
-        # try:
-        #     loop = asyncio.get_event_loop()
-        #     loop.create_task(
-        #             sendSocketMessage(
-        #                 code=0,
-        #                 msg="抠图结束",
-        #                 data={
-        #                     "status": "已完成",
-        #                     "goods_art_nos": goods_arts,
-        #                 },
-        #                 msg_type="segment_progress",
-        #             )
-        #         )
-        # except:
-        #     asyncio.run(
-        #         sendSocketMessage(
-        #             code=0,
-        #             msg="抠图结束",
-        #             data={
-        #                 "status": "已完成",
-        #                 "goods_art_nos": goods_arts,
-        #             },
-        #             msg_type="segment_progress",
-        #         )
-        #     )
         callback_func("已结束抠图处理")
         return True
 
@@ -471,7 +584,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):
 
         # =============
         # 整体数据校验,返回错误内容,以及
@@ -522,18 +635,13 @@ class RunMain:
         _result = {"code": 99, "message": "无法解析到数据,请检查登录企业"}
         print("is_use_excel", is_use_excel)
         if not is_use_excel:
-            if settings.PROJECT == "红蜻蜓":
-                # goods_no_dict输出为文件夹下涉及到的所有款为key的字典,后续通过解析字典,进行提取对应文件夹
+            if is_detail == 1:
                 _result = self.data_mode_generate_detail.get_basic_goods_art_data_by_hqt_and_hlm(
-                    folder_name_list
-                )
-
-            elif settings.PROJECT == "惠利玛":
-                if settings.Company:
-                    if "惠利玛" in settings.Company:
-                        _result = self.data_mode_generate_detail.get_basic_goods_art_data_by_hqt_and_hlm(
-                            folder_name_list
-                        )
+                                folder_name_list
+                            )
+            else:
+                _result = self.data_mode_generate_detail.makeFakeData(folder_name_list)
+                print("fake data _result", _result)
         else:
             keys = settings.keys
             _result = (
@@ -543,7 +651,7 @@ class RunMain:
                     keys,
                 )
             )
-            print("打印=======>>>>>>>_result=============>", _result)
+        print("打印=======>>>>>>>_result=============>", _result)
         if _result["code"] == 0:
             remote_data = _result["data"]
         else:
@@ -561,7 +669,23 @@ class RunMain:
         print("temp_class====>", temp_class)
         print("temp_class====>", temp_name)
         # 获取所有文件夹基础数据内容  检查不满足要求的文件不满足要求移动到错误文件夹
-        need_view_list = temp_class[temp_name].need_view
+        # 在访问 temp_class[temp_name].need_view 前增加检查
+        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
         )
@@ -604,14 +728,14 @@ class RunMain:
                         "folder_path": "{}/{}".format(image_dir, one_folder),
                     }
                 )
-                return_data["message"] += "文件夹:{} 找不到对应数据\n".format(
+                return_data["message"] += "文件夹:{} 在系统资料中找不到对应数据\n".format(
                     one_folder
                 )
                 return_data["data"]["config_data"]["success_handler"].append(
                     {
                         "goods_art_no": one_folder,
                         "success": False,
-                        "info": f"文件夹:{one_folder} 找不到对应数据",
+                        "info": f"文件夹:{one_folder} 在系统资料中找不到对应数据",
                     }
                 )
 
@@ -647,7 +771,7 @@ class RunMain:
             # 款号反向映射;因为部分key键格式为KUM9999999
             _x = {}
             for i, v in goods_no_dict.items():
-                _x[v["款号"]] = i
+                _x[str(v["款号"])] = i
 
             for goods_no, value in error_data_dict.items():
                 if goods_no in _x:
@@ -675,7 +799,7 @@ class RunMain:
 
         # 如果没有有效数据则进行退出
         if not goods_no_dict:
-            return_data["message"] += "没有任何有效数据\n"
+            return_data["message"] += "在系统资料中没有查询到有效数据\n"
             return return_data
 
         # 校验无误的文件夹数据  goods_no_dict为最终有效数据
@@ -732,8 +856,11 @@ class RunMain:
             if goods_no not in goods_no_need_temps:
                 continue
             for __temp_name in goods_no_need_temps[goods_no]:
-                _path = "{}/{}/{}/{}".format(
-                    image_dir, "软件-详情图生成", __temp_name, goods_no
+                # _path = "{}/{}/{}/{}".format(
+                #     image_dir, "软件-详情图生成", __temp_name, goods_no
+                # )
+                _path = "{}/{}/切片图-{}".format(
+                    image_dir, f"详情图-{goods_no}",__temp_name
                 )
                 if not os.path.exists(_path):
                     print("款号详情图不存在", _path)
@@ -774,8 +901,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---------------")
@@ -790,13 +915,47 @@ class RunMain:
 
         return_data["code"] = 0
         return return_data
-
+    def sendAsyncMessage(self,msg="", goods_arts=[], status="",progress={}):
+        """异步发送消息"""
+        data = {
+            "code": 0,
+            "msg": msg,
+            "status": 2,
+            "data": {
+                "status": status,
+                "goods_art_nos": goods_arts,
+            },
+            "progress":{
+                    "msg_type":"detail_progress",
+                    "name":"详情页",
+                    "goods_art_no":progress.get("goods_art_no"),
+                    "status":progress.get("status"),
+                    "current":progress.get("current",0),
+                    "total":progress.get("total",0),
+                    "error":progress.get("error",0),
+                    "folder":progress.get("folder",None)
+                },
+            "msg_type": "detail_progress",
+        }
+        print("\033[1;32;40m 详情页消息 \033[0m",data)
+        message_queue.put_nowait(data)
     def move_error_folders(self, one_path, target_folder, message=""):
         if os.path.exists(one_path):
             check_path(target_folder)
             move_folders(path_list=[one_path], target_folder=target_folder)
-
-    def check_for_detail_first_call_back(self, data):
+    '''
+    total_progress = len(all_goods_art_no_folder_data)
+        finish_progress = 0
+        error_progress = 0
+        progress = {"status":"正在处理",
+                "current":finish_progress,
+                "total":total_progress,
+                "error":error_progress}
+        sendAsyncMessage(
+            msg="开始处理抠图", goods_arts=goods_art_nos, status="开始处理",progress=progress
+        )
+    '''
+    def check_for_detail_first_call_back(self, data,request_parmas):
         # 首次数据校验的信息返回
         # self.show_message(text="22222222222222222222222")
         # QMessageBox.critical(self, "警告", "1111111", QMessageBox.Ok)
@@ -867,7 +1026,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"],
@@ -889,6 +1048,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()
@@ -908,6 +1068,7 @@ class RunMain:
         assigned_page_dict,
         excel_temp_goods_no_data,
         finally_goods_no_need_temps,
+        request_parmas
     ):
         """
         excel_temp_goods_no_data: {},  # 表格数据可能存在多模板,数据结构为一个款号下的多个模板的数据列表
@@ -922,19 +1083,44 @@ class RunMain:
         image_dir = config_data["image_dir"]
 
         # 详情图生成结果文件夹
-        out_put_dir = "{}\软件-详情图生成".format(image_dir)
+        # out_put_dir = "{}\软件-详情图生成".format(image_dir)
+        out_put_dir = "{}".format(image_dir)
         if settings.IS_TEST:
             print("==============_goods_no_dict  打印=================")
 
             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())
+        detail_finish_progress = 0
+        detail_error_progress = 0
+        detail_progress = {"status":"正在处理", "current":detail_finish_progress, "total":detail_total_progress, "error":detail_error_progress}
+        self.sendAsyncMessage(
+            msg="准备处理详情页",
+            goods_arts=[],
+            status="准备处理详情页",
+            progress=detail_progress
+        )
         for goods_no, temp_name_list in finally_goods_no_need_temps.items():
             for _temp_name in temp_name_list:
                 try:
+                    detail_finish_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(
+                        msg="正在处理详情页",
+                        goods_arts=[],
+                        status="正在处理详情页",
+                        progress=detail_progress
+                    )
                     # if _temp_name != "xiaosushuoxie-4":
                     #     continue
                     assigned_page_list = []
@@ -953,12 +1139,16 @@ class RunMain:
                                     if _key in temp_info_data:
                                         temp_info_data[_key] = _key_value
                     print("goods_no:{},_temp_name:{}".format(goods_no, _temp_name))
-                    out_put_dir_resp = "{}/详情模板{}/{}".format(
-                        out_put_dir, _temp_name, goods_no
-                    )
-                    all_detail_path_list.append(
-                        "{}/详情模板{}/{}".format(out_put_dir, _temp_name, goods_no)
+                    # out_put_dir_resp = "{}/详情模板{}/{}".format(
+                    #     out_put_dir, _temp_name, goods_no
+                    # )
+                    out_put_dir_resp = "{}/详情图-{}".format(
+                        out_put_dir, goods_no
                     )
+                    # all_detail_path_list.append(
+                    #     "{}/详情模板{}/{}".format(out_put_dir, _temp_name, goods_no)
+                    # )
+                    all_detail_path_list.append(out_put_dir_resp)
                     # continue
                     self.detail_deal_one_data(
                         goods_no=goods_no,
@@ -973,6 +1163,13 @@ class RunMain:
                     config_data["success_handler"].append(
                         {"goods_art_no": goods_no, "success": True, "info": "处理成功"}
                     )
+                    detail_progress["folder"] = out_put_dir_resp
+                    self.sendAsyncMessage(
+                        msg="开始处理详情页",
+                        goods_arts=[],
+                        status="开始处理详情页",
+                        progress=detail_progress
+                    )
                     recordDataPoint(
                         token=self.token,
                         uuid=self.uuid,
@@ -980,6 +1177,15 @@ 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(
+                        msg="处理失败",
+                        goods_arts=[],
+                        status="处理失败",
+                        progress=detail_progress
+                    )
                     self.show_progress_detail(
                         {
                             "goods_art_no": goods_no,
@@ -987,7 +1193,6 @@ class RunMain:
                             "info": "款:{}生成详情异常:{}".format(goods_no, e),
                         }
                     )
-                    print(e)
                     # raise UnicornException("款:{}生成详情异常:{}".format(goods_no, e))
                     config_data["success_handler"].append(
                         {
@@ -1025,132 +1230,23 @@ class RunMain:
         print("out_put_dir_resp", out_put_dir_resp)
         # 打开文件夹
         # os.startfile(out_put_dir)
-
+        detail_finish_progress = self.total_num - self.fail_num
+        text_status = "处理完成" if detail_finish_progress > 0 else "处理失败"
+        detail_progress = {
+            "status":text_status, 
+            "current":detail_finish_progress, 
+            "total":self.total_num, 
+            "error":self.fail_num,
+            "folder":out_put_dir_resp[0] if text_status=="处理完成" else "",
+        }
+        self.sendAsyncMessage(
+            msg=text_status,
+            goods_arts=[],
+            status=text_status,
+            progress=detail_progress
+        )
         return config_data
 
-    def detail_run_by_thread11111(
-        self,
-        config_data,
-        _goods_no_dict,
-        temp_name,
-        temp_name_list,
-        assigned_page_dict,
-        excel_temp_goods_no_data,
-        finally_goods_no_need_temps,
-    ):
-        """
-        excel_temp_goods_no_data: {},  # 表格数据可能存在多模板,数据结构为一个款号下的多个模板的数据列表
-        finally_goods_no_need_temps: {},  # 每个款号需要生成的模板数据
-        """
-
-        # 开始处理
-        self.n = 0
-        self.total_num = len(_goods_no_dict)
-        self.fail_num = 0
-        is_use_excel = config_data["is_use_excel"]
-        image_dir = config_data["image_dir"]
-
-        # 详情图生成结果文件夹
-        out_put_dir = "{}\软件-详情图生成".format(image_dir)
-        if settings.IS_TEST:
-            print("==============_goods_no_dict  打印=================")
-
-            print(json.dumps(_goods_no_dict))
-
-            print("==============_goods_no_dict  打印-end=================")
-
-        if settings.IS_TEST:
-            max_workers = 1
-        else:
-            max_workers = 1
-
-        all_detail_path_list = []
-
-        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
-            futures = []
-            for goods_no, temp_name_list in finally_goods_no_need_temps.items():
-                for _temp_name in temp_name_list:
-                    # if _temp_name != "xiaosushuoxie-4":
-                    #     continue
-
-                    assigned_page_list = []
-                    if _temp_name in assigned_page_dict:
-                        assigned_page_list = assigned_page_dict[_temp_name]
-                    # 如果为使用表格,则获取表格中的数据作为款号的基础数据
-                    temp_info_data = copy.copy(_goods_no_dict[goods_no])
-                    if is_use_excel:
-                        # 将表格中的数据进行替换
-                        if goods_no in excel_temp_goods_no_data:
-                            if _temp_name in excel_temp_goods_no_data[goods_no]:
-                                # 将表格中的特定的模板的行,替换到goods_no的data中,因为不同的模板有数据特殊性
-                                print("xxxxxx====>", excel_temp_goods_no_data[goods_no])
-                                for _key, _key_value in excel_temp_goods_no_data[
-                                    goods_no
-                                ][_temp_name].items():
-                                    if _key in temp_info_data:
-                                        temp_info_data[_key] = _key_value
-
-                    print("temp_info_data11111111111111111111111")
-                    print("goods_no:{},_temp_name:{}".format(goods_no, _temp_name))
-                    all_detail_path_list.append(
-                        "{}/{}/".format(out_put_dir, _temp_name, goods_no)
-                    )
-                    # continue
-                    futures.append(
-                        executor.submit(
-                            self.detail_deal_one_data,
-                            goods_no=goods_no,
-                            value=temp_info_data,
-                            out_put_dir=out_put_dir,
-                            temp_name=_temp_name,
-                            assigned_page_list=assigned_page_list,
-                        )
-                    )
-
-            # for goods_no, value in _goods_no_dict.items():
-            #     _temp_name = temp_name
-            #     # 使用自定义的表格数据
-            #     if self.isUseTemplate is False:
-            #         if "模板名称" in value:
-            #             if value["模板名称"] in temp_name_list:
-            #                 _temp_name = value["模板名称"]
-            #     assigned_page_list = []
-            #     if _temp_name in assigned_page_dict:
-            #         assigned_page_list = assigned_page_dict[_temp_name]
-            #
-            #     futures.append(executor.submit(
-            #         self.deal_one_data,
-            #         goods_no=goods_no,
-            #         value=value,
-            #         out_put_dir=out_put_dir,
-            #         temp_name=_temp_name,
-            #         assigned_page_list=assigned_page_list,
-            #     ))
-
-            # 使用 wait 方法等待所有任务完成
-            done, not_done = concurrent.futures.wait(futures)
-
-            # 处理完成的任务
-            for future in done:
-                if settings.IS_TEST:
-                    result = future.result()
-
-        # ==============完成处理==============
-        self.set_state(state_value=2)
-        if self.total_num:
-            if self.fail_num:
-                self.show_progress_detail(
-                    "处理完成,-----------处理失败数据:{}个款".format(self.fail_num)
-                )
-            else:
-                self.show_progress_detail("处理完成")
-        else:
-            self.show_progress_detail("没有任何数据")
-
-        config_data["sign_text"] = "已结束详情处理"
-        config_data["all_detail_path_list"] = all_detail_path_list
-        self.run_end_sign.emit(config_data)
-
     def show_progress_detail(self, text):
         # self.show_progress_detail_sign.emit(text)
         # self.windows.show_progress_detail(text)
@@ -1168,29 +1264,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)
-        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:
+        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 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,
@@ -1212,7 +1309,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:

+ 158 - 35
python/settings.py

@@ -2,11 +2,12 @@ from wsgiref import headers
 from dotenv import load_dotenv, find_dotenv
 from pathlib import Path  # Python 3.6+ only
 import configparser, json, pytz
-import requests
+import requests,os
 import pillow_avif
 from utils.common import message_queue
 import hashlib
-
+from pathlib import Path
+import os,stat
 TIME_ZONE = pytz.timezone("Asia/Shanghai")
 from numpy import true_divide
 from databases import (
@@ -19,7 +20,11 @@ from databases import (
     batch_insert_device_configs,
     batch_insert_device_configsNew,
 )
-
+USER_TOKEN = None  # 可以通过外部设置或从配置文件中加载
+USER_ENV = None  # 可以通过外部设置或从配置文件中加载
+USER_CAMERAS = False  # 可以通过外部设置或从配置文件中加载
+# 追加配置参数 machine_type 拍照机设备类型;0鞋;1服装
+MACHINE_TYPE = 0
 # 初始化数据表
 create_all_database()
 session = SqlQuery()
@@ -46,6 +51,7 @@ else:
             config = SysConfigs(**sys_config)
             session.add(config)
             session.commit()  # 合并事务提交
+session.close()
 # 初始化数据表---结束
 
 
@@ -59,17 +65,50 @@ def get_config_by_items(config_dict):
 keys = ["面料", "里料"]
 
 
+def sync_sys_configs2Online():
+    hlm_token = USER_TOKEN
+    headers = {
+        "Authorization": f"Bearer {hlm_token}",
+        "content-type": "application/json",
+    }
+    # 追加配置参数 machine_type 拍照机设备类型;0鞋;1服装
+    session = SqlQuery()
+    sysConfigs = CRUD(SysConfigs)
+    all_configs = sysConfigs.read_all(session)
+    localConfigData = {}
+    for local_config in all_configs:
+        localConfigData[local_config.key] = json.loads(local_config.value)
+    data_json = json.dumps(
+        {"configs": localConfigData, "machine_type": MACHINE_TYPE},
+        ensure_ascii=False,
+    )
+    # 同步本地到线上
+    url = DOMAIN + "/api/ai_image/camera_machine/update_all_user_configs"
+    requests.post(url=url, headers=headers, data=data_json)
+    session.close()
+
+
 def getSysConfigs(key, item, default=None):
     session = SqlQuery()
     crud = CRUD(SysConfigs)
     one_item = crud.read(session, conditions={"key": key})
     config = json.loads(one_item.value)
+    session.close()
     if item == "image_out_format":
         default_format = config.get(item, default)
         if default_format == "" or default_format == None:
             return "png"
     return config.get(item, default)
-
+def updateSysConfigs(params):
+    session = SqlQuery()
+    sysConfig = CRUD(SysConfigs)
+    # 如果value是字典或列表,需要转换为JSON字符串
+    if "value" in params and isinstance(params["value"], (dict, list)):
+        params["value"] = json.dumps(params["value"], ensure_ascii=False)
+    # 走编辑逻辑
+    # kws = params.__dict__
+    sysConfig.updateConditions(session, conditions={"key": params["key"]}, **params)
+    session.close()
 
 def get_dict_value(_dict, key, default=None):
     if key in _dict:
@@ -194,11 +233,11 @@ MOVE_DOWN = _mcu_config_dict["move_down"]
 STOP = _mcu_config_dict["stop"]
 
 
-camera_config_dict = config.items("camera_config")
-_camera_config_dict = {}
-for i, k in mcu_config_dict:
-    _camera_config_dict[i] = int(k)
-_camera_config_dict = get_config_by_items(camera_config_dict)
+# camera_config_dict = config.items("camera_config")
+# _camera_config_dict = {}
+# for i, k in mcu_config_dict:
+#     _camera_config_dict[i] = int(k)
+# _camera_config_dict = get_config_by_items(camera_config_dict)
 
 # LOW_ISO = _camera_config_dict["low_iso"]
 # HIGH_ISO = _camera_config_dict["high_iso"]
@@ -207,7 +246,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 = "惠利玛"
 
@@ -246,30 +286,6 @@ CUTOUT_MODE = (
 )
 import importlib
 
-# plugins = [
-#     # "custom_plugins.plugins.detail_template.huilima.detail_huilima1.DetailPicGet",
-#     ".custom_plugins.plugins.detail_template.huilima.detail_huilima1.DetailPicGet",
-#     # "custom_plugins.plugins.detail_template.huilima.detail_huilima2.DetailPicGet",
-#     # "custom_plugins.plugins.detail_template.huilima.detail_huilima3.DetailPicGet",
-#     # "custom_plugins.plugins.detail_template.huilima.detail_huilima4.DetailPicGet",
-#     # "custom_plugins.plugins.detail_template.xiaosushuoxie.detail_xiaosushuoxie1",
-#     # "custom_plugins.plugins.detail_template.xiaosushuoxie.detail_xiaosushuoxie2",
-#     # "custom_plugins.plugins.detail_template.xiaosushuoxie.detail_xiaosushuoxie3",
-#     # "custom_plugins.plugins.detail_template.xiaosushuoxie.detail_xiaosushuoxie4",
-#     # "custom_plugins.plugins.detail_template.xiaosushuoxie.detail_xiaosushuoxie5",
-#     # "custom_plugins.plugins.detail_template.xiaosushuoxie.detail_xiaosushuoxie6",
-#     # "custom_plugins.plugins.detail_template.hongqingting.detail_hongqingting1.DetailPicGet",
-#     # "custom_plugins.plugins_mode.detail_generate_base.DetailBase",
-#     # "custom_plugins.plugins_mode.pic_deal.PictureProcessing",
-# ]
-# def load_plugin(plugin_path):
-#     module_name, class_name = plugin_path.rsplit(".", 1)
-#     module = importlib.import_module(module_name)
-#     return getattr(module, class_name)
-
-
-# loaded_plugins = [load_plugin(p.lstrip(".")) for p in plugins]
-
 OUT_PIC_QUALITY = "普通"
 GRENERATE_MAIN_PIC_BRIGHTNESS = int(
     getSysConfigs("other_configs", "grenerate_main_pic_brightness", 254)
@@ -292,7 +308,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=""):
     """记录日志"""
@@ -316,7 +336,49 @@ 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 syncBatchPhotoRecordDelete():
+    """同步图片记录"""
+    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 = {"machine_type": machine_type, "action_type": 4}
+    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=""
@@ -341,3 +403,64 @@ def calculate_md5(filepath):
             md5hash.update(chunk)
         # 返回MD5哈希的十六进制表示
         return md5hash.hexdigest()
+__output_dir = config.get("output_config", "output_dir")
+path_obj = Path(os.path.abspath(__output_dir))
+OUTPUT_DIR = path_obj.absolute()
+print("OUTPUT_DIR",__output_dir,OUTPUT_DIR)
+def handle_remove_readonly(func, path, exc):
+    os.chmod(path, stat.S_IWRITE)
+    func(path)
+
+try:
+    __scan_dir = config.get("scan_config", "scan_dir")
+    if __scan_dir == "" or __scan_dir is None:
+        SCAN_DIR = None
+    else:
+        scan_path_obj = Path(os.path.abspath(__scan_dir))
+        SCAN_DIR = scan_path_obj.absolute()
+        if not os.path.exists(SCAN_DIR):
+            # 创建多级目录
+            os.makedirs(SCAN_DIR, exist_ok=True)
+except:
+    SCAN_DIR = None
+    print('扫码配置不存在')
+print("SCAN_DIR",SCAN_DIR)
+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)

+ 10 - 2
python/sockets/connect_manager.py

@@ -1,6 +1,6 @@
 from models import WebSocket
 from logger import logger
-import json, asyncio
+import json, asyncio,time
 from starlette.websockets import WebSocketState
 class ConnectionManager:
     is_connected = False
@@ -27,7 +27,15 @@ class ConnectionManager:
     async def send_personal_message(self, message: str, websocket: WebSocket):
         '''向用户发送消息'''
         # await websocket.send_json(message)
-        await websocket.send_json(message)
+        t_send_start = time.time()
+        try:
+            print(f"[T4: {time.time()-t_send_start:.4f}s] 开始 websocket.send_json")
+            await websocket.send_json(message)
+            await asyncio.sleep(0) 
+            # print(f"[T5: {time.time()-t_send_start:.4f}s] websocket.send_json 返回",message)
+        except Exception as e:
+            logger.info(f"socket 消息发送异常:{str(e)}")
+            await asyncio.sleep(0.001)
 
     async def broadcast(self, message: str):
         """广播消息"""

+ 345 - 74
python/sockets/message_handler.py

@@ -16,43 +16,168 @@ import settings
 from middleware import UnicornException
 from concurrent.futures import ThreadPoolExecutor
 from functools import partial
-
+from logger import logger
+import stat
 # 创建全局线程池
 executor = ThreadPoolExecutor(max_workers=4)
 async def handlerCutOut(
     manager=None, run_main=None, config_data={}, websocket=None, msg_type=""
 ):
-    try:
-        # return_data = run_main.check_before_cutout(config_data)
-        # await run_main.check_for_cutout_image_first_call_back(return_data)
-        # 将阻塞操作放到线程池中执行
-        loop = asyncio.get_event_loop()
-        return_data = await loop.run_in_executor(
-            executor, partial(run_main.check_before_cutout, config_data)
-        )
-        # await run_main.check_for_cutout_image_first_call_back(return_data)
-        await loop.run_in_executor(
-            executor,
-            partial(run_main.check_for_cutout_image_first_call_back, return_data),
-        )
-    except UnicornException as e:
-        data = manager.jsonMessage(
-            code=1,
-            msg=e.msg,
-            msg_type=msg_type,
-        )
-        await manager.send_personal_message(data, websocket)
-        return
-    except Exception as e:
-        print("error",e)
-        data = manager.jsonMessage(
-            code=1,
-            msg="抠图异常,请稍后重试~",
-            msg_type=msg_type,
-        )
-        await manager.send_personal_message(data, websocket)
-
+    max_retry_count = 1  # 最多重试1次
+    retry_count = 0
+    
+    while retry_count <= max_retry_count:
+        try:
+            if retry_count > 0:
+                logger.info(f"抠图操作重试第{retry_count}次")
+            
+            # return_data = run_main.check_before_cutout(config_data)
+            # await run_main.check_for_cutout_image_first_call_back(return_data)
+            # 将阻塞操作放到线程池中执行
+            loop = asyncio.get_event_loop()
+            return_data = await loop.run_in_executor(
+                executor, partial(run_main.check_before_cutout, config_data)
+            )
+            # await run_main.check_for_cutout_image_first_call_back(return_data)
+            await loop.run_in_executor(
+                executor,
+                partial(run_main.check_for_cutout_image_first_call_back, return_data),
+            )
+            # 成功则退出循环
+            break
+        except UnicornException as e:
+            logger.error(f"抠图操作发生UnicornException: {e.msg}")
+            logger.error(f"异常类型: UnicornException")
+            logger.error(f"当前重试次数: {retry_count}/{max_retry_count}")
+            
+            # data = manager.jsonMessage(
+            #     code=1,
+            #     msg=e.msg,
+            #     msg_type=msg_type,
+            # )
+            # await manager.send_personal_message(data, websocket)
+            return
+        except FileNotFoundError as e:
+            error_msg = str(e)
+            logger.error(f"抠图操作发生FileNotFoundError: {error_msg}")
+            logger.error(f"异常类型: FileNotFoundError")
+            logger.error(f"当前重试次数: {retry_count}/{max_retry_count}")
+            
+            # 检查是否是原始图缺失错误
+            if "原始图" in error_msg and retry_count < max_retry_count:
+                logger.info("检测到原始图缺失,尝试重新拷贝原图并重试...")
+                try:
+                    # 重新拷贝原图
+                    goods_art_nos = config_data.get("goods_art_nos", [])
+                    image_dir = config_data.get("image_dir", "")
+                    
+                    for goods_art_no in goods_art_nos:
+                        dealImage = DealImage(image_dir)
+                        resFlag, path = dealImage.dealMoveImageV2(
+                            goods_art_no=goods_art_no,
+                        )
+                        if not resFlag:
+                            logger.error(f"重新拷贝原图失败: {goods_art_no}")
+                            raise UnicornException(f"重新拷贝原图失败: {goods_art_no}")
+                    
+                    logger.info("原图重新拷贝成功,准备重试抠图操作")
+                    retry_count += 1
+                    continue  # 重试
+                except Exception as copy_error:
+                    logger.error(f"重新拷贝原图时发生错误: {str(copy_error)}")
+                    return
+            else:
+                logger.error(f"重新拷贝原图时发生错误: 非原始图错误或已达到最大重试次数")
+                return
+        except Exception as e:
+            import traceback
+            error_msg = str(e)
+            stack_trace = traceback.format_exc()
+            
+            logger.error(f"抠图操作发生未知异常: {error_msg}")
+            logger.error(f"异常类型: {type(e).__name__}")
+            logger.error(f"当前重试次数: {retry_count}/{max_retry_count}")
+            logger.error(f"完整堆栈跟踪:\n{stack_trace}")
+            
+            # 如果是第一次执行且未达到最大重试次数,则重试
+            if retry_count < max_retry_count:
+                logger.info("准备重试抠图操作...")
+                retry_count += 1
+                continue
+            return
 
+def handlerFolderDelete(limit_path, goods_art_no_arrays, is_write_txt_log):
+    check_path(limit_path)
+    move_folder_array = check_move_goods_art_no_folder(
+        "output", goods_art_no_arrays, limit_path
+    )
+    
+    for goods_art_revice in goods_art_no_arrays:
+        cutout_goods = f"{limit_path}/{goods_art_revice}"
+        if os.path.exists(cutout_goods):
+            for root, dirs, files in os.walk(cutout_goods):
+                for file in files:
+                    filepath = os.path.join(root, file)
+                    os.chmod(filepath, stat.S_IWRITE)
+            logger.info(f"解除占用并开始删除目录:{cutout_goods}")
+            # 尝试多次删除,增加成功率
+            retry_count = 3
+            while retry_count > 0:
+                try:
+                    shutil.rmtree(cutout_goods, onerror=settings.handle_remove_readonly)
+                    del move_folder_array[goods_art_revice]
+                    break
+                except OSError as e:
+                    logger.info(f"目录已经被删除-OSError:{str(e)}")
+                    break
+                except (PermissionError) as e:
+                    retry_count -= 1
+                    if retry_count == 0:
+                        logger.info(f"抠图前目录删除出现问题-PermissionError:{str(e)};{goods_art_revice};{cutout_goods}")
+                        if is_write_txt_log:
+                            error_file_path = f"{cutout_goods}/异常说明-出现目录丢失或缺少图片请点开查看原因.txt"
+                            with open(error_file_path, 'w', encoding='utf-8') as f:
+                                f.write("目录删除失败\n")
+                                f.write(f"原因: 文件被占用或没有删除权限\n")
+                                f.write(f"错误信息: {str(e)}\n")
+                                f.write(f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
+                                f.write(f"请关闭可能正在使用此目录的程序后,为当前货号点击重拍后重试\n")
+                        else:
+                            raise UnicornException(f"目录检查出现问题:{str(e)},请关闭错误提示中的被占用文件")
+                    else:
+                        logger.info(f"抠图前目录删除出现问题--PermissionError:{str(e)};{retry_count}")
+                        time.sleep(0.5)  # 等待0.5秒后重试
+                except Exception as e:
+                    retry_count -= 1
+                    if retry_count == 0:
+                        logger.info(f"抠图前目录删除出现问题--Exception:{str(e)};{goods_art_revice};{cutout_goods}")
+                        if is_write_txt_log:
+                            error_file_path = f"{cutout_goods}/异常说明-出现目录丢失或缺少图片请点开查看原因.txt"
+                            with open(error_file_path, 'w', encoding='utf-8') as f:
+                                f.write("目录删除失败\n")
+                                f.write(f"原因: {str(e)}\n")
+                                f.write(f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
+                                f.write(f"请检查目录状态后,为当前货号点击重拍后重试\n")
+                        else:
+                            raise UnicornException(f"目录检查出现问题:{str(e)},请关闭错误提示中的被占用文件")
+                    else:
+                        logger.info(f"抠图前目录删除出现问题--Exception:{str(e)};{retry_count}")
+                        time.sleep(0.5)  # 等待0.5秒后重试
+    
+    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,
@@ -63,12 +188,15 @@ async def handlerSend(
     loop = asyncio.get_event_loop()
     receiveData = json.loads(receiveData)
     # 处理消息发送逻辑
-    receiveData = json.loads(receiveData.get("text"))
-    print("receiveData", receiveData)
-    jsonType = receiveData.get("type")
-    code = receiveData.get("code")
-    msg = receiveData.get("msg")
-    data = receiveData.get("data")
+    try:
+        receiveData = json.loads(receiveData.get("text"))
+        print("receiveData", receiveData)
+        jsonType = receiveData.get("type")
+        code = receiveData.get("code")
+        msg = receiveData.get("msg")
+        data = receiveData.get("data")
+    except:
+        jsonType = "ping"
 
     match jsonType:
         case "ping":
@@ -102,8 +230,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")
@@ -126,6 +263,9 @@ async def handlerSend(
                 device_ctrl.is_stop_action = True
             else:
                 print("动作没有执行,略过")
+                data = manager.jsonMessage(code=0, msg="执行终止", msg_type="run_mcu_stop")
+                await manager.send_personal_message(data, websocket)
+                return
         case "run_mcu":
             msg_type = "run_mcu"
             action_info = data.get("action", "执行左脚程序")
@@ -137,6 +277,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(
@@ -149,7 +296,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(
@@ -170,7 +317,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
             )
@@ -180,6 +328,7 @@ async def handlerSend(
                 ),
                 name="run_mcu_config",
             )
+            session.close()
         case "run_mcu_single":
             device_ctrl = DeviceControl(
                 websocket_manager=manager, smart_shooter=smart_shooter
@@ -190,16 +339,51 @@ async def handlerSend(
                 ),
                 name="run_mcu_single",
             )
+        case "get_device_info":
+            device_ctrl = DeviceControl(
+                websocket_manager=manager, smart_shooter=smart_shooter
+            )
+            device_ctrl.get_device_info()
         case "handler_take_picture":
+            if data is None:
+                PointName = "A"
+            else:
+                PointName = data.get("point_name", "A")
             device_ctrl = DeviceControl(
                 websocket_manager=manager, smart_shooter=smart_shooter
             )
+            print("收到单拍指令",'handler_take_picture')
             await device_ctrl.controlDevice("laser_position", 0)
             blue_tooth = BlueToothMode(
                 websocket_manager=manager, smart_shooter=smart_shooter
             )
+            session = SqlQuery()
+            crud = CRUD(PhotoRecord)
+            record = crud.read(session=session, order_by="id", ascending=False,conditions={"delete_time": None})
+            if record == None:
+                # 发送失败消息
+                data = manager.jsonMessage(
+                            code=1,
+                            msg="单拍失败,请先输入货号或扫码进行组合拍摄",
+                            msg_type="handler_take_picture",
+                        )
+                await manager.send_personal_message(data, websocket)
+                return
+            try:
+                limit_path = "{}/{}".format(settings.OUTPUT_DIR,
+                    time.strftime("%Y-%m-%d", time.localtime(time.time()))
+                )
+                move_folder_array = handlerFolderDelete(limit_path,[record.goods_art_no],False)
+            except UnicornException as e:
+                data = manager.jsonMessage(
+                        code=1,
+                        msg=e.msg,
+                        msg_type="handler_take_picture",
+                    )
+                await manager.send_personal_message(data, websocket)
+                return
             loop.create_task(
-                blue_tooth.remote_control_v2.handlerTakePhoto(smart_shooter),
+                blue_tooth.remote_control_v2.handlerTakePhoto(smart_shooter,session,record,PointName),
                 name="run_mcu_config",
             )
             await asyncio.sleep(2.5)
@@ -209,7 +393,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,
@@ -248,6 +432,7 @@ async def handlerSend(
                 ),
                 name="run_mcu_config_single",
             )
+            session.close()
         case "get_deviation":
             device_ctrl = DeviceControl(
                 websocket_manager=manager, smart_shooter=smart_shooter
@@ -304,8 +489,16 @@ async def handlerSend(
             """
             获取相机信息,是否连接
             """
+            # token
+            # env
+            # print("smart_shooter_getinfo",data)
+            # isMultCameraMode = data.get("isMultiCameraMode",False)
             loop.create_task(
-                smart_shooter.GetCameraInfo(msg_type="smart_shooter_getinfo"),
+                smart_shooter.GetCameraInfo(
+                    msg_type="smart_shooter_getinfo",
+                    is_send=True,
+                    # isMultCameraMode=isMultCameraMode,
+                ),
                 name="smart_shooter_getinfo",
             )
         case "smart_shooter_enable_preview":
@@ -313,19 +506,72 @@ async def handlerSend(
             启动相机或关闭实时预览
             """
             value = data.get("value", True)
+            if data is None:
+                PointName = "A"
+            else:
+                PointName = data.get("point_name", "A")
+            logger.info(f"启动相机或关闭实时预览,PointName:{PointName},value:{value}")
+            camera_configs = settings.getSysConfigs(
+                "camera_configs",
+                "iso_config",
+                None,
+            )
+            msg_type = "smart_shooter_enable_preview_status"
+            print("传入得msg_type====>>>", msg_type)
+            temp_A_point = camera_configs.get(PointName, None)
+            CameraKey = temp_A_point.get("CameraKey", None) if temp_A_point else None
             loop.create_task(
                 smart_shooter.EnableCameraPreview(
-                    enable_status=value, msg_type="smart_shooter_enable_preview"
+                    enable_status=value,
+                    msg_type=msg_type,
+                    CameraKey=CameraKey,
                 ),
-                name="smart_shooter_enable_preview",
+                name=msg_type,
             )
+        case "smart_shooter_auto_focus":
+            """
+            启动相机或关闭实时预览
+            """
+            if data is None:
+                PointName = "A"
+            else:
+                PointName = data.get("point_name", "A")
+            camera_configs = settings.getSysConfigs(
+                "camera_configs",
+                "iso_config",
+                None,
+            )
+            msg_type = "smart_shooter_auto_focus"
+            temp_A_point = camera_configs.get(PointName, None)
+            CameraKey = temp_A_point.get("CameraKey", None) if temp_A_point else None
+            status,msg = await  smart_shooter.CameraAutofocus(
+                    CameraKey=CameraKey,
+                )
+            if not status:
+                data = manager.jsonMessage(
+                    code=1,
+                    msg=msg,
+                    msg_type=msg_type,
+                )
+                await manager.send_personal_message(data, websocket)
         case "smart_shooter_get_camera_property":
             """
             启动相机或关闭实时预览
             """
+            if data is None:
+                PointName = "A"
+            else:
+                PointName = data.get("point_name", "A")
+            camera_configs = settings.getSysConfigs(
+                "camera_configs",
+                "iso_config",
+                None,
+            )
+            temp_A_point = camera_configs.get(PointName, None)
+            CameraKey = temp_A_point.get("CameraKey", None) if temp_A_point else None
             msg_type = "smart_shooter_get_camera_property"
             code = 0
-            status, info = await smart_shooter.GetCameraProperty()
+            status, info = await smart_shooter.GetCameraProperty(CameraKey=CameraKey)
             code = 1 if status == False else 0
             msg = info if status == False else "操作成功"
             data = info if status == True else {}
@@ -340,6 +586,17 @@ async def handlerSend(
             """
             获取相机信息,是否连接
             """
+            if data is None:
+                PointName = "A"
+            else:
+                PointName = data.get("point_name", "A")
+            camera_configs = settings.getSysConfigs(
+                "camera_configs",
+                "iso_config",
+                None,
+            )
+            temp_A_point = camera_configs.get(PointName, None)
+            CameraKey = temp_A_point.get("CameraKey", None) if temp_A_point else None
             device_ctrl = DeviceControl(
                 websocket_manager=manager, smart_shooter=smart_shooter
             )
@@ -347,13 +604,28 @@ async def handlerSend(
             # 兼容主图测试
             id = data.get("id", 0)
             goods_art_no = data.get("goods_art_no", "")
+            if goods_art_no:
+                try:
+                    limit_path = "{}/{}".format(settings.OUTPUT_DIR,
+                        time.strftime("%Y-%m-%d", time.localtime(time.time()))
+                    )
+                    move_folder_array = handlerFolderDelete(limit_path,[goods_art_no],False)
+                except UnicornException as e:
+                    data = manager.jsonMessage(
+                            code=1,
+                            msg=e.msg,
+                            msg_type="smart_shooter_photo_take",
+                        )
+                    await manager.send_personal_message(data, websocket)
+                    return
             is_af = True
             loop.create_task(
                 smart_shooter.CameraShooter(
-                    msg_type="smart_shooter_photo_take",
+                    msg_type="run_mcu",
                     id=id,
                     goods_art_no=goods_art_no,
                     is_af=is_af,
+                    CameraKey=CameraKey,
                 ),
                 name="smart_shooter_photo_take",
             )
@@ -365,7 +637,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,
@@ -376,7 +648,7 @@ async def handlerSend(
                 return
             reset_data = {"image_path": None}
             photoRecord.update(session, id, **reset_data)
-            device_ctrl = DeviceControl(websocket_manager=manager)
+            device_ctrl = DeviceControl(websocket_manager=manager)#
             loop.create_task(
                 device_ctrl.only_take_photo(
                     goods_art_no=goods_art_no,
@@ -385,6 +657,7 @@ async def handlerSend(
                 ),
                 name="sendCommand",
             )
+            session.close()
         case "segment_progress":
             msg_type = "segment_progress"
             obj = None
@@ -393,29 +666,28 @@ async def handlerSend(
             uuid = data.get("uuid", "")
             run_main = RunMain(obj, token, uuid)
             goods_art_no_arrays = data.get("goods_art_no", [])
-            limit_path = "output/{}".format(
+            limit_path = "{}/{}".format(settings.OUTPUT_DIR,
                 time.strftime("%Y-%m-%d", time.localtime(time.time()))
             )
-            check_path(limit_path)
+            try:
+                move_folder_array = handlerFolderDelete(limit_path,goods_art_no_arrays,True)
+            except UnicornException as e:
+                data = manager.jsonMessage(
+                        code=1,
+                        msg=e.msg,
+                        msg_type=msg_type,
+                    )
+                await manager.send_personal_message(data, websocket)
+                return
             # 该数组表示是否需要后面的移动文件夹操作,减少重复抠图,提升抠图时间和速度
-            move_folder_array = check_move_goods_art_no_folder(
-                "output", goods_art_no_arrays, limit_path
-            )
-            for goods_art_revice in goods_art_no_arrays:
-                cutout_goods = f"{limit_path}/{goods_art_revice}"
-                if os.path.exists(cutout_goods):
-                    # 寻找当前被扣图的货号在现有目录中是否存在,如果存在先删除
-                    # 重新执行抠图操作
-                    shutil.rmtree(cutout_goods)
-                    del move_folder_array[goods_art_revice]
+            session = SqlQuery()
             for goods_art_no in 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})
                 if not images:
                     data = manager.jsonMessage(
                         code=1,
-                        msg=f"没有可用货号数据",
+                        msg=f"商品货号【{goods_art_no}】在商品档案资料中不存在,请检查货号是否正确",
                         msg_type=msg_type,
                     )
                     await manager.send_personal_message(data, websocket)
@@ -432,17 +704,15 @@ async def handlerSend(
                             )
                             await manager.send_personal_message(data, websocket)
                             return
-                        new_file_name = (
-                            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)
+                        # 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,
+                    resFlag, path = dealImage.dealMoveImageV2(
                         goods_art_no=goods_art_no,
                     )
                     if not resFlag:
@@ -454,6 +724,7 @@ async def handlerSend(
                         )
                         await manager.send_personal_message(data, websocket)
                         return
+            session.close()
             # try:
             cutOutMode = (
                 "1"

+ 1 - 1
python/sockets/socket_client.py

@@ -3,7 +3,7 @@ from enum import Flag
 import socket, json, asyncio
 import websockets
 from settings import APP_HOST,PORT
-from middleware import UnicornException
+# from middleware import UnicornException
 class SocketClient:
 
     def __init__(self, uri="ws://127.0.0.1:7074"):

+ 49 - 49
python/sockets/socket_server.py

@@ -12,15 +12,14 @@ from sqlalchemy.exc import NoResultFound
 import os, datetime
 import traceback
 import logging
+from utils import common
+from utils.common import message_queue
 logger = logging.getLogger(__name__)
 conn_manager = ConnectionManager()
 active_connections = set()
 device_ctrl = DeviceControl(websocket_manager=conn_manager)
 blue_tooth = BlueToothMode(websocket_manager=conn_manager)
 smart_shooter = SmartShooter(websocket_manager=conn_manager)
-from utils.common import message_queue
-
-
 async def updateDataRecord(PhotoFilename, id):
     await asyncio.sleep(0.01)
     create_time = datetime.datetime.fromtimestamp(os.path.getctime(PhotoFilename))
@@ -28,47 +27,72 @@ 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()
 
 @app.websocket("/ws")
 async def websocket_endpoint(websocket: WebSocket):
     # await websocket.accept()
+    main_loop = asyncio.get_running_loop()
+    smart_shooter.main_loop = main_loop  # <--- 添加这一行
     await conn_manager.connect(websocket)
     active_connections.add(websocket)
     smart_shooter.websocket = websocket
     device_ctrl.websocket = websocket
     blue_tooth.websocket = websocket
+    common.websocket_manager = conn_manager
+    common.websocket = websocket
     # 启动 smart_shooter.connect_listen 服务
     listen_task = None
     tasks = set()
+    send_task = None # <--- 新增
     try:
         # 初始化回调函数
         smart_shooter.callback_listen = MsgCallback
         # 创建任务来并发处理不同类型的消息
         handler_task = asyncio.create_task(handler_messages(websocket))
         # send_task = asyncio.create_task(send_message(websocket))
+        send_task = asyncio.create_task(send_message(websocket)) # <--- 启动消费者
         loop = asyncio.get_event_loop()
         listen_task = loop.run_in_executor(None, smart_shooter.connect_listen)
         # send_task = loop.run_in_executor(None, send_message(websocket))
         # 创建任务来启动 connect_listen
         # listen_task = asyncio.create_task(restart_smart_shooter_listener())
         # 等待所有任务完成
-        await asyncio.gather(handler_task, listen_task)
+        await asyncio.gather(handler_task,listen_task, return_exceptions=True)
 
     except WebSocketDisconnect:
         print("Client disconnected")
+    except asyncio.CancelledError:
+        print("Connection cancelled")
     finally:
-        # 确保任务被正确取消
-        if listen_task and not listen_task.done():
-            listen_task.cancel()
+        # 确保任务被正确取消和清理
+        tasks_to_cancel = []
+        
+        if handler_task and not handler_task.done():
+            tasks_to_cancel.append(handler_task)
+        
+        if send_task and not send_task.done():
+            tasks_to_cancel.append(send_task)
+        
+        # 取消所有待处理的任务
+        for task in tasks_to_cancel:
+            task.cancel()
+        
+        # 等待任务取消完成
+        if tasks_to_cancel:
+            await asyncio.gather(*tasks_to_cancel, return_exceptions=True)
+        
+        # 清理连接
         active_connections.discard(websocket)
+        print("WebSocket connection cleaned up")
 
 
 async def start_smart_shooter_listen():
@@ -88,16 +112,9 @@ async def handler_messages(websocket):
             socket_type = byteDats.get("type")
             if socket_type == "websocket.disconnect":
                 print("socket_type===>", byteDats)
-                smart_shooter.stop_listen = True
-                smart_shooter.is_init_while = False
                 if byteDats.get("code") == 1006:
                     continue
-                device_ctrl.close_connect()
-                device_ctrl.close_lineConnect()
-                device_ctrl.mcu_exit = True
-                device_ctrl.p_list = []
-                device_ctrl.temp_ports_dict = {}
-                device_ctrl.clearMyInstance()
+                logger.info("socket强制断开")
                 diviceList = blue_tooth.devices
                 if len(diviceList) == 0:
                     blue_tooth.bluetooth_exit = True
@@ -128,31 +145,12 @@ async def handler_messages(websocket):
             break
 
 
-# async def send_message(websocket):
-#     print("构建消息监听   send_message")
-#     while True:
-#         try:
-#             # 使用wait()而不是直接get()来避免阻塞
-#             # 从异步队列中获取消息(在新事件循环中运行)
-#             message = await message_queue.get()
-#             # 发送消息
-#             await websocket.send_json(message)
-#             message_queue.task_done()
-#         except asyncio.QueueEmpty:
-#             continue
-#         except asyncio.TimeoutError:
-#             # 超时继续循环,避免永久阻塞
-#             continue
-#         except Exception as e:
-#             print("socket报错",e)
-#             break
 async def message_generator():
     """异步生成器,用于从队列中获取消息"""
     while True:
         try:
             # 使用asyncio.wait_for设置合理的超时时间
-            message = await asyncio.wait_for(message_queue.get(), timeout=0.1)
-            print("获取消息中",message)
+            message = await message_queue.get()
             yield message
         except asyncio.TimeoutError:
             # 超时继续,允许其他协程运行
@@ -167,20 +165,21 @@ async def message_generator():
 async def send_message(websocket):
     """使用异步生成器发送消息"""
     print("构建消息监听   send_message")
-    async for message in message_generator():
+    while True:
         try:
-            # 检查WebSocket连接状态
-            if websocket.client_state.name != "CONNECTED":
-                print("WebSocket连接已断开,停止发送消息")
-                continue
-            print("发送消息中。。。。。", message)
-            # 发送消息
-            await websocket.send_json(message)
+            # 1. 异步等待消息,不会阻塞其他协程
+            data = await message_queue.get()
+            
+            # 2. 发送消息
+            if common.websocket_manager:
+                # 假设 broadcast 或 send_personal_message 是异步方法
+                await common.websocket_manager.send_personal_message(data,common.websocket) 
+                # 或者根据你的 ConnectionManager 实现调用具体发送方法
+            
             message_queue.task_done()
-            print("消息发送完成...")
         except Exception as e:
-            print("socket报错", e)
-            break
+            print(f"消息消费错误: {e}")
+            await asyncio.sleep(1) # 防止死循环报错
 
 
 async def MsgCallback(msg):
@@ -195,8 +194,9 @@ async def MsgCallback(msg):
             ):
                 # temp_photo_name = PhotoFilename
                 # 更新拍照记录
-                print("PhotoFilename", PhotoFilename, PhotoOrigin)
+                logger.info(f"PhotoUpdated,{PhotoFilename}--{PhotoOrigin}")
                 goods_art_no = None
+                id = None
                 try:
                     if PhotoOrigin != "" and PhotoOrigin not in ["external", "ui"]:
                         goods_art_no, id = PhotoOrigin.split(",")

+ 40 - 126
python/temp.py

@@ -1,127 +1,41 @@
-# from PIL import Image
-# from settings import recordDataPoint
-from service.online_request.module_online_data import OnlineDataRequest,AIGCDataRequest
+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)
+import zmq, json
+import asyncio, settings
+
+# # ... existing code ...
+from mcu.capture.smart_shooter_class import SmartShooter
+
+
+async def main():
+    sm = SmartShooter(None)
+    await sm.GetCameraInfo()
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
 
-tempData = {
-    "code": 0,
-    "message": "",
-    "data": {
-        "error_folder_list": [],
-        "goods_no_dict": {
-            "A596371111": {
-                "款号": "A596371",
-                "货号资料": [
-                    {
-                        "货号": "A596371",
-                        "文件夹名称": "A596371",
-                        " 编号": "",
-                        "颜色名称": "黑色",
-                        "pics": {
-                            "俯视-抠图": "output/2025-08-20/A596371/阴影图处理/A596371(1)_俯视_抠图.png",
-                            "俯视-阴影": "output/2025-08-20/A596371/阴影图处理/A596371(1)_俯视_阴影.png",
-                            "侧视-抠图": "output/2025-08-20/A596371/阴影图处理/A596371(2)_侧视_抠图.png",
-                            "侧视-阴影": "output/2025-08-20/A596371/阴影图处理/A596371(2)_侧视_阴影.png",
-                            "后跟-抠图": "output/2025-08-20/A596371/阴影图处理/A596371(3)_后跟_抠图.png",
-                            "后跟-阴影": "output/2025-08-20/A596371/阴影图处理/A596371(3)_后跟_阴影.png",
-                            "鞋底-抠图": "output/2025-08-20/A596371/阴影图处理/A596371(4)_鞋底_抠图.png",
-                            "鞋底-阴影": "output/2025-08-20/A596371/阴影图处理/A596371(4)_鞋底_阴影.png",
-                            "内里-抠图": "output/2025-08-20/A596371/阴影图处理/A596371(5)_内里_抠图.png",
-                            "内里-阴影": "output/2025-08-20/A596371/阴影图处理/A596371(5)_内里_阴影.png",
-                            "其他1-抠图": "output/2025-08-20/A596371/阴影图处理/A596371(6)_其他1_抠图.png",
-                            "其他1-阴影": "output/2025-08-20/A596371/阴影图处理/A596371(6)_其他1_阴影.png",
-                            "其他0-抠图": "output/2025-08-20/A596371/阴影图处理/A596371(7)_其他0_抠图.png",
-                            "其他0-阴影": "output/2025-08-20/A596371/阴影图处理/A596371(7)_其他0_阴影.png",
-                        },
-                        "800x800": [
-                            "output/2025-08-20/A596371/800x800/A596371(1)_800.png",
-                            "output/2025-08-20/A596371/800x800/A596371(2)_800.png",
-                            "output/2025-08-20/A596371/800x800/A596371(3)_800.png",
-                            "output/2025-08-20/A596371/800x800/A596371(4)_800.png",
-                            "output/2025-08-20/A596371/800x800/A596371(5)_800.png",
-                            "output/2025-08-20/A596371/800x800/A596371(6)_800.png",
-                            "output/2025-08-20/A596371/800x800/A596371(7)_800.png",
-                        ],
-                    }
-                ],
-                "商品面料": "",
-                "商品内里": "",
-                "商品鞋底": "",
-                "鞋垫": "",
-                "商品标题": "女单鞋",
-                "商品价格": "300.00",
-            }
-        },
-        "succeed_folder_list": ["A596371"],
-        "temp_name": "huilima-1",
-        "temp_name_list": [
-            "huilima-1",
-            "huilima-2",
-            "huilima-3",
-            "huilima-4",
-            "xinnuo-1",
-            "xinnuo-2",
-            "xinnuo-3",
-            "huilima-6",
-            "xinjunlue-1",
-            "qingyangyizhan-1",
-        ],
-        "assigned_page_dict": {},
-        "excel_temp_goods_no_data": {},
-        "finally_goods_no_need_temps": {"A596371": ["huilima-1"]},
-        "config_data": {
-            "image_dir": "output/2025-08-20",
-            "image_order": "俯视,侧视,后跟,鞋底,内里,组合,组合2",
-            "goods_art_no": "",
-            "goods_art_nos": ["A596371"],
-            "is_check_number": False,
-            "resize_image_view": "后跟",
-            "cutout_mode": 1,
-            "logo_path": "",
-            "special_goods_art_no_folder_line": "",
-            "is_use_excel": False,
-            "excel_path": "",
-            "is_check_color_is_all": False,
-            "cutout_is_pass": True,
-            "assigned_page_dict": {},
-            "detail_is_pass": True,
-            "upload_is_pass": False,
-            "upload_is_enable": True,
-            "is_filter": False,
-            "temp_class": {
-                "huilima-1": "custom_plugins.plugins.detail_template.huilima.detail_huilima1.DetailPicGet",
-                "huilima-2": "custom_plugins.plugins.detail_template.huilima.detail_huilima2.DetailPicGet",
-                "huilima-3": "custom_plugins.plugins.detail_template.huilima.detail_huilima3.DetailPicGet",
-                "huilima-4": "custom_plugins.plugins.detail_template.huilima.detail_huilima4.DetailPicGet",
-                "xinnuo-1": "custom_plugins.plugins.detail_template.xinnuo.detail_xinnuo1.DetailPicGet",
-                "xinnuo-2": "custom_plugins.plugins.detail_template.xinnuo.detail_xinnuo2.DetailPicGet",
-                "xinnuo-3": "custom_plugins.plugins.detail_template.xinnuo.detail_xinnuo3.DetailPicGet",
-                "huilima-6": "custom_plugins.plugins.detail_template.huilima.detail_huilima6.DetailPicGet",
-                "xinjunlue-1": "custom_plugins.plugins.detail_template.xinjunlue.detail_xinjunlue1.DetailPicGet",
-                "qingyangyizhan-1": "custom_plugins.plugins.detail_template.qingyangyizhan.detail_qingyangyizhan1.DetailPicGet",
-            },
-            "temp_name": "huilima-1",
-            "temp_name_list": [
-                "huilima-1",
-                "huilima-2",
-                "huilima-3",
-                "huilima-4",
-                "xinnuo-1",
-                "xinnuo-2",
-                "xinnuo-3",
-                "huilima-6",
-                "xinjunlue-1",
-                "qingyangyizhan-1",
-            ],
-            "target_error_folder": "output/2025-08-20/软件-生成详情错误",
-            "success_handler": [],
-            "sign_text": "",
-        },
-    },
-}
-goods_no_dict = tempData["data"]["goods_no_dict"]
-params = []
-token = "Bearer 18323b96c68234597b1fa8d10fecb6bbe45cadc3"
-# onlineData = OnlineDataRequest(token)
-# aigc_clazz = AIGCDataRequest(token)
-# res = aigc_clazz.searchProgress(1120457)
-# print("res", res)

+ 13 - 1
python/utils/common.py

@@ -1,3 +1,15 @@
 import asyncio, queue
-
 message_queue = asyncio.Queue()
+websocket_manager = None
+websocket = None
+
+# def sendSocketMessage(data):
+#         payload = data
+#         loop = asyncio.get_event_loop()
+#         if websocket_manager == None:
+#             loop.create_task(message_queue.put(payload))
+#         else:
+#             async def _do_send():
+#                 await websocket_manager.send_personal_message(payload, websocket_manager)
+#             loop.create_task(_do_send())
+#         print("\033[1;32;40m common===>sendSocketMessage \033[0m", data)

+ 109 - 0
python/utils/utils_func.py

@@ -240,3 +240,112 @@ def get_cutout_image_info(source_image_path):
     data["cutout_image_file_name"] = cutout_image_file_name
     data["cutout_image_file_extension"] = cutout_image_file_extension
     return data
+
+
+def dynamic_parameter_issuance_get(receive_data):
+    # 动态解析参数,并进行下发,第一个为功能码,后续为参数。
+    func_code = receive_data[1] << 8 | receive_data[2]
+    status_code = receive_data[3] << 8 | receive_data[4]
+    par_len = receive_data[5]
+    par_data = receive_data[6:]
+    out_par_data_list = []
+    if par_len > 0:
+        chunk_size = len(par_data) // par_len
+        par_data_list = [
+            par_data[i : i + chunk_size] for i in range(0, len(par_data), chunk_size)
+        ]
+        for one_par in par_data_list:
+            start = 0
+            _value = (
+                one_par[start] << 40
+                | one_par[start + 1] << 32
+                | one_par[start + 2] << 24
+                | one_par[start + 3] << 16
+                | one_par[start + 4] << 8
+                | one_par[start + 5]
+            )
+            start = start + 5
+            _dir = 1 if one_par[start + 1] == 1 else -1
+            _type = "int" if one_par[start + 2] == 1 else "float"
+            _precision = one_par[start + 3]
+            if _dir < 0:
+                _value = _value * _dir
+            if _type == "float":
+                if _precision > 0:
+                    _round_x = _precision
+                    _precision = _precision * -1
+                    _value = _value * 10**_precision
+                    _value = round(_value, _round_x)
+
+            out_par_data_list.append(_value)
+    return func_code, status_code, out_par_data_list
+
+
+def calculation_value(value=None):
+    buf = []
+    # 地址、正负、类型(整数、浮点)、精度(0.01)、数据(6个字节)、是否只读
+    _dir = 1 if value >= 0 else 0  # 是否正负
+    _type = 1 if isinstance(value, int) else 0  # 1是整数
+    if _type == 0:
+        _precision = len(str(value).split(".")[1])  # 精度
+        if _precision > 4:
+            _precision = 4
+        abs_int_value = int(abs(value * 10**_precision))
+    else:
+        abs_int_value = abs(value)
+        _precision = 0
+    buf.extend(
+        [
+            0xFF & abs_int_value >> 40,
+            0xFF & abs_int_value >> 32,
+            0xFF & abs_int_value >> 24,
+            0xFF & abs_int_value >> 16,
+            0xFF & abs_int_value >> 8,
+            0xFF & abs_int_value,
+        ]
+    )
+    buf.extend([_dir, _type, _precision])
+    # print(abs_int_value, _dir, _type, _precision)
+    return buf
+
+
+def read_cmd_test(receive_data):
+    if len(receive_data) < 4:
+        return False
+
+    if receive_data[0] == 0x55 and receive_data[1] == 0x55:
+        data_len = receive_data[2]
+        if len(receive_data) < data_len + 4:
+            return False
+        _data = receive_data[3 : data_len + 4]
+        # 校验数据
+        if 0xFF & ~sum(_data[:-1]) == _data[-1]:
+            return _data[:-1]
+        else:
+            return False
+    else:
+        return False
+
+
+# 动态参数生成
+def dynamic_parameter_issuance_send(func_code, status_code, par_data_list=None):
+    # 动态解析参数,并进行下发,第一个为功能码,后续为参数。
+    _buf = [
+        0xFF & func_code >> 8,
+        0xFF & func_code,
+        0xFF & status_code >> 8,
+        0xFF & status_code,
+        0 if not par_data_list else len(par_data_list),
+    ]
+    if par_data_list:
+        for par_data in par_data_list:
+            _buf.extend(calculation_value(par_data))
+    return _buf
+
+
+def cmd_send_test(data):
+    buf = []
+    buf.extend([0x55, 0x55, (0xFF & len(data))])
+    buf.extend(data)
+    buf.extend([0xFF & ~sum(data)])
+    return buf

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác