Преглед изворни кода

feat(category-links): 添加分类关联模块及电子秤名称配置化

- 新增 CategoryLinks 数据模型及数据库表定义
- 添加分类关联的查询、新增/修改、删除三个 API 接口
- 新增 CategoryLinksReq 请求模型,支持 id 为空时自动走新增逻辑
- 保存时自动同步 tab_name 与 device_config_tabs 表中的 mode_name
- 将电子秤串口名称从硬编码改为从 config.ini 配置读取
- 恢复串口扫描日志输出便于调试
- 添加分类关联接口文档
rambo пре 12 часа
родитељ
комит
d4eaf1e9a5

+ 156 - 0
doc/分类关联接口文档-20261023.md

@@ -0,0 +1,156 @@
+# 分类关联(CategoryLinks)接口文档
+
+> 日期:2026-10-23
+> 接口实现:`python/api.py`(`get_category_links` / `save_category_link` / `delete_category_link`)
+> 请求模型:`python/models.py`(`CategoryLinksReq`)
+> 数据表模型:`python/model/category_links.py`(`CategoryLinks`)
+
+---
+
+## 一、功能说明
+
+`category_links` 表用于将**分类名称**与 `device_config_tabs` 表中的 **Tab 名称(mode_name)和 id** 进行关联。
+
+- 新增 / 修改时会**校验 `tab_id` 在 `device_config_tabs` 表中是否存在**,不存在则拒绝写入
+- 保存时自动从 Tab 记录同步 `tab_name = mode_name`,保证名称与 id 始终一致
+- 新增时校验 `category_name` 不允许重复
+
+### 数据表结构(category_links)
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| `id` | int(主键,自增) | 记录id |
+| `category_name` | str(128) | 分类名称,不允许为空、不允许重复 |
+| `tab_name` | str | 关联的Tab名称(自动同步自 `device_config_tabs.mode_name`) |
+| `tab_id` | int | 关联的Tab配置id(对应 `device_config_tabs.id`) |
+
+### 统一返回格式
+
+```json
+{
+  "code": 0,      // 0=成功,1=失败
+  "msg": "",      // 提示信息
+  "data": {}      // 业务数据
+}
+```
+
+---
+
+## 二、接口列表
+
+### 1. 查询分类关联列表
+
+- **接口地址**:`GET /get_category_links`
+- **接口说明**:查询所有分类与Tab配置的关联记录,按 `id` 升序返回
+- **请求参数**:无
+
+**响应示例**:
+
+```json
+{
+  "code": 0,
+  "msg": "",
+  "data": {
+    "list": [
+      {
+        "id": 1,
+        "category_name": "运动鞋",
+        "tab_name": "执行左脚程序",
+        "tab_id": 1
+      }
+    ]
+  }
+}
+```
+
+---
+
+### 2. 新增或修改分类关联
+
+- **接口地址**:`POST /save_category_link`
+- **接口说明**:`id` 为空或 0 时走新增逻辑,否则走编辑逻辑
+- **请求体**(`CategoryLinksReq`,JSON):
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| `id` | int | 否 | 关联记录id,为空则新增 |
+| `category_name` | str | 是 | 分类名称,不能为空 |
+| `tab_id` | int | 是 | 关联的Tab配置id(`device_config_tabs` 表) |
+
+**请求示例**:
+
+```json
+{
+  "id": null,
+  "category_name": "运动鞋",
+  "tab_id": 1
+}
+```
+
+**校验规则**:
+
+| 场景 | 返回 |
+|---|---|
+| `category_name` 为空 | `{"code": 1, "msg": "分类名称不能为空"}` |
+| `tab_id` 在 `device_config_tabs` 中不存在 | `{"code": 1, "msg": "关联的Tab配置不存在,请检查tab_id"}` |
+| 新增时分类名称已存在 | `{"code": 1, "msg": "该分类名称已存在,请勿重复添加"}` |
+| 编辑时记录不存在 | `{"code": 1, "msg": "数据不存在"}` |
+
+**成功响应示例**:
+
+```json
+{
+  "code": 0,
+  "msg": "操作成功",
+  "data": {
+    "id": 1,
+    "category_name": "运动鞋",
+    "tab_name": "执行左脚程序",
+    "tab_id": 1
+  }
+}
+```
+
+> 说明:`tab_name` 由服务端根据 `tab_id` 自动填充,前端无需传递。
+
+---
+
+### 3. 删除分类关联
+
+- **接口地址**:`POST /delete_category_link`
+- **接口说明**:根据 `id` 删除一条分类与Tab配置的关联记录
+- **请求体**(`ModelGetDeviceConfigDetail`,JSON):
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| `id` | int | 是 | 关联记录id |
+
+**请求示例**:
+
+```json
+{
+  "id": 1
+}
+```
+
+**校验规则**:
+
+| 场景 | 返回 |
+|---|---|
+| 记录不存在 | `{"code": 1, "msg": "数据不存在"}` |
+
+**成功响应示例**:
+
+```json
+{
+  "code": 0,
+  "msg": "删除成功",
+  "data": null
+}
+```
+
+---
+
+## 三、关联表说明(device_config_tabs)
+
+Tab 列表可通过现有接口 `GET /get_device_tabs?type={0|1}` 获取(`type`:0左脚 / 1右脚),返回的 `tabs` 数组中每项的 `id`、`mode_name` 即为新增分类关联时 `tab_id` 的合法取值来源。

+ 59 - 1
python/api.py

@@ -19,7 +19,7 @@ from io import BytesIO
 from sqlalchemy import and_, asc, desc
 from functools import partial
 from service.deal_image import DealImage
-from databases import DeviceConfig, SysConfigs, SqlQuery, CRUD, select, DeviceConfigTabs
+from databases import DeviceConfig, SysConfigs, SqlQuery, CRUD, select, DeviceConfigTabs, CategoryLinks
 from service.run_main import RunMain
 import importlib
 from service.auto_deal_pics.upload_pic import UploadPic
@@ -1058,6 +1058,64 @@ def update_tab_name(params: DeviceConfigTabsReq):
     }
 
 
+@app.get("/get_category_links", description="获取分类关联列表")
+def get_category_links():
+    """查询所有分类与Tab配置的关联记录(category_links表)"""
+    session = SqlQuery()
+    linkModel = CRUD(CategoryLinks)
+    links = linkModel.read_all(session, order_by="id", ascending=True)
+    session.close()
+    return {"code": 0, "msg": "", "data": {"list": links}}
+
+
+@app.post("/save_category_link", description="新增或修改分类关联")
+def save_category_link(params: CategoryLinksReq):
+    """新增或修改分类与Tab配置的关联,校验tab_id在device_config_tabs表中是否存在"""
+    category_name = (params.category_name or "").strip()
+    if category_name == "":
+        return {"code": 1, "msg": "分类名称不能为空", "data": None}
+    session = SqlQuery()
+    # 校验tab_id在device_config_tabs表中是否存在
+    tabsModel = CRUD(DeviceConfigTabs)
+    tab = tabsModel.read(session, conditions={"id": params.tab_id})
+    if tab == None:
+        session.close()
+        return {"code": 1, "msg": "关联的Tab配置不存在,请检查tab_id", "data": None}
+    linkModel = CRUD(CategoryLinks)
+    # 同步Tab最新名称,保证tab_name与device_config_tabs表中的mode_name一致
+    kwargs = {"category_name": category_name, "tab_id": tab.id, "tab_name": tab.mode_name}
+    if params.id == None or params.id == 0:
+        # 走新增逻辑,分类名称不允许重复
+        exist = linkModel.read(session, conditions={"category_name": category_name})
+        if exist != None:
+            session.close()
+            return {"code": 1, "msg": "该分类名称已存在,请勿重复添加", "data": None}
+        save_link = linkModel.create(session, obj_in=kwargs)
+    else:
+        # 走编辑逻辑,先校验记录是否存在
+        model = linkModel.read(session, conditions={"id": params.id})
+        if model == None:
+            session.close()
+            return {"code": 1, "msg": "数据不存在", "data": None}
+        save_link = linkModel.update(session, obj_id=params.id, **kwargs)
+    session.close()
+    return {"code": 0, "msg": "操作成功", "data": save_link}
+
+
+@app.post("/delete_category_link", description="删除分类关联")
+def delete_category_link(params: ModelGetDeviceConfigDetail):
+    """根据id删除一条分类与Tab配置的关联记录"""
+    session = SqlQuery()
+    linkModel = CRUD(CategoryLinks)
+    model = linkModel.read(session, conditions={"id": params.id})
+    if model == None:
+        session.close()
+        return {"code": 1, "msg": "数据不存在", "data": None}
+    linkModel.delete(session, obj_id=params.id)
+    session.close()
+    return {"code": 0, "msg": "删除成功", "data": None}
+
+
 @app.post("/get_device_configs", description="获取可执行程序命令列表")
 def get_device_configs(params: ModelGetDeviceConfig):
     tab_id = params.tab_id

+ 4 - 1
python/config.ini

@@ -53,4 +53,7 @@ output_dir = ..\..\..\output\
 template_url = http://localhost:3001
 
 [scan_config]
-scan_dir = ..\..\..\scans\
+scan_dir = ..\..\..\scans\
+
+[electronic_scale]
+scale_name = "Prolific USB-to-Serial Comm Port"

+ 1 - 1
python/databases.py

@@ -8,7 +8,7 @@ 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
+from model import DeviceConfig, PhotoRecord, SysConfigs, DeviceConfigTabs, CategoryLinks
 
 check_path("C:/Zhihuiyin")
 # 创建SQLite数据库引擎

+ 4 - 4
python/mcu/DeviceControl.py

@@ -1208,7 +1208,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
         ports = serial.tools.list_ports.comports()
         # 遍历所有端口并打印信息
         for port in ports:
-            # logger.info("扫描到的串口信息:{}".format(port.description))
+            logger.info("扫描到的串口信息:{}".format(port.description))
             if "CH34" in port.description:
                 ports_dict[port.name] = {
                     "name": port.name,
@@ -1219,7 +1219,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                     "product": port.product,
                     "serial_number": port.serial_number,
                 }
-            elif "Prolific USB-to-Serial Comm Port" in port.description:
+            elif settings.SCALE_NAME in port.description:
                 ports_dict[port.name] = {
                     "name": port.name,
                     "device": port.device,
@@ -1344,7 +1344,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 connect_flag = receive_data[1]
                 device_id = receive_data[2]
         else:
-            if "Prolific USB-to-Serial Comm Port" in _port_value.get('description'):
+            if settings.SCALE_NAME in _port_value.get('description'):
                 # 电子秤不会应答探测命令,收不到数据属正常现象,落到下方统一连接
                 pass
             else:
@@ -1391,7 +1391,7 @@ class DeviceControl(BaseClass, metaclass=SingletonType):
                 return True
 
         else:
-            if "Prolific USB-to-Serial Comm Port" in _port_value.get('description'):
+            if settings.SCALE_NAME in _port_value.get('description'):
                 # 探测用的临时串口已在上方 close(Windows同一COM口不能重复打开)
                 # probe_connect 内部以9600重新打开并校验安衡协议特征,
                 # 确认是电子秤才真正连接,避免误占其他 Prolific 设备的串口

+ 1 - 0
python/model/__init__.py

@@ -2,3 +2,4 @@ from .device_config import DeviceConfig
 from .photo_record import PhotoRecord
 from .sys_configs import SysConfigs
 from .device_config_tabs import DeviceConfigTabs
+from .category_links import CategoryLinks

+ 12 - 0
python/model/category_links.py

@@ -0,0 +1,12 @@
+from sqlmodel import SQLModel, Field, Text
+from datetime import datetime
+from typing import Optional
+
+
+# 系统相关配置表
+class CategoryLinks(SQLModel, table=True):
+    __tablename__ = "category_links"
+    id: Optional[int] = Field(default=None, primary_key=True, index=True)
+    category_name: str = Field(max_length=128, nullable=False)
+    tab_name: str = Field(default=None)
+    tab_id: int = Field(default=0)

+ 8 - 0
python/models.py

@@ -143,6 +143,14 @@ class DeviceConfigTabsReq(BaseModel):
     mode_name: str = Field(default="", description="Tab名称")
 
 
+class CategoryLinksReq(BaseModel):
+    """分类关联配置-新增或修改"""
+
+    id: Optional[int] = Field(default=None, description="关联记录id,为空则新增")
+    category_name: str = Field(default="", description="分类名称")
+    tab_id: int = Field(default=0, description="关联的Tab配置id(device_config_tabs表)")
+
+
 class RecordUpdate(BaseModel):
     """系统配置"""
 

+ 2 - 1
python/settings.py

@@ -218,7 +218,8 @@ BACKUP_COUNTS = config.get("log", "backup_counts")
 HLM_HOST = config.get("log", "hlm_host")
 
 PROJECT = config.get("app", "project")
-
+# 电子秤配置名称
+SCALE_NAME = config.get("electronic_scale", "scale_name")
 # ----------------------------------
 mcu_config_dict = config.items("mcu_config")
 _mcu_config_dict = {}