Просмотр исходного кода

refactor(category-links): 将分类关联存储从独立表迁移至sys_configs

- 删除 CategoryLinks 数据模型及对应数据库表
- 新增 _get_category_links_list 和 _save_category_links_list 辅助函数,通过 sys_configs 读写 JSON 数组
- 查询接口改为解析 sys_configs 中 category_links 配置返回数组
- 新增/修改接口以 category_name 作为唯一键匹配,增加 tab_id 唯一性校验
- 删除接口改为按数组下标删除,新增 CategoryLinksDelete 请求模型
- 增删改操作后均调用 sync_sys_configs2Online 同步线上配置
- 请求模型增加 token 字段用于线上同步鉴权
- settings.py 同步接口增加响应内容打印便于调试
- 更新接口文档反映存储结构与业务规则变更
rambo 2 часов назад
Родитель
Сommit
6f19890912

+ 44 - 35
doc/分类关联接口文档-20261023.md

@@ -1,28 +1,32 @@
-# 分类关联(CategoryLinks)接口文档
+# 分类关联(category_links)接口文档
 
 > 日期: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`)
+> 请求模型:`python/models.py`(`CategoryLinksReq`、`CategoryLinksDelete`)
 
 ---
 
 ## 一、功能说明
 
-`category_links` 表用于将**分类名称**与 `device_config_tabs` 表中的 **Tab 名称(mode_name)和 id** 进行关联。
+分类与Tab配置的关联数据**不再使用独立数据表**,改为存储在 `sys_configs` 表中:
 
-- 新增 / 修改时会**校验 `tab_id` 在 `device_config_tabs` 表中是否存在**,不存在则拒绝写入
-- 保存时自动从 Tab 记录同步 `tab_name = mode_name`,保证名称与 id 始终一致
-- 新增时校验 `category_name` 不允许重复
+- **key**:`category_links`
+- **value**(JSON字符串):
+
+```json
+[
+  {"category_name": "运动鞋", "tab_name": "执行左脚程序", "tab_id": 1}
+]
+```
 
-### 数据表结构(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`) |
+- 新增 / 修改时会**校验 `tab_id` 在 `device_config_tabs` 表中是否存在**,不存在则拒绝写入
+- **`tab_id` 唯一性校验**:若该 `tab_id` 已被其他分类关联,报错提示"该Tab已被分类【xxx】关联,请勿重复关联"
+- 以 `category_name` 作为唯一键:同名分类走修改(覆盖原关联),不存在则新增(追加到数组末尾)
+- 保存时自动从 Tab 记录同步 `tab_name = mode_name`,保证名称与 id 始终一致
+- **增删改成功后均调用 `settings.sync_sys_configs2Online()` 同步到线上**(与其他 sys_configs 配置一致)
+- 新增/修改/删除接口需传入 `token`(用户token),接口内赋值给 `settings.USER_TOKEN`,供线上同步鉴权使用
 
 ### 统一返回格式
 
@@ -41,7 +45,7 @@
 ### 1. 查询分类关联列表
 
 - **接口地址**:`GET /get_category_links`
-- **接口说明**:查询所有分类与Tab配置的关联记录,按 `id` 升序返回
+- **接口说明**:读取 `sys_configs` 中 `category_links` 配置并解析为数组返回;配置不存在时返回空数组
 - **请求参数**:无
 
 **响应示例**:
@@ -53,7 +57,6 @@
   "data": {
     "list": [
       {
-        "id": 1,
         "category_name": "运动鞋",
         "tab_name": "执行左脚程序",
         "tab_id": 1
@@ -68,22 +71,22 @@
 ### 2. 新增或修改分类关联
 
 - **接口地址**:`POST /save_category_link`
-- **接口说明**:`id` 为空或 0 时走新增逻辑,否则走编辑逻辑
+- **接口说明**:按 `category_name` 匹配已有记录,存在则修改,不存在则新增;成功后同步线上配置
 - **请求体**(`CategoryLinksReq`,JSON):
 
 | 字段 | 类型 | 必填 | 说明 |
 |---|---|---|---|
-| `id` | int | 否 | 关联记录id,为空则新增 |
-| `category_name` | str | 是 | 分类名称,不能为空 |
+| `category_name` | str | 是 | 分类名称,唯一键 |
 | `tab_id` | int | 是 | 关联的Tab配置id(`device_config_tabs` 表) |
+| `token` | str | 是 | 用户token,用于保存后同步线上配置鉴权 |
 
 **请求示例**:
 
 ```json
 {
-  "id": null,
   "category_name": "运动鞋",
-  "tab_id": 1
+  "tab_id": 1,
+  "token": "60db012c9dd228dd0b08bea6e6fc915d"
 }
 ```
 
@@ -93,20 +96,22 @@
 |---|---|
 | `category_name` 为空 | `{"code": 1, "msg": "分类名称不能为空"}` |
 | `tab_id` 在 `device_config_tabs` 中不存在 | `{"code": 1, "msg": "关联的Tab配置不存在,请检查tab_id"}` |
-| 新增时分类名称已存在 | `{"code": 1, "msg": "该分类名称已存在,请勿重复添加"}` |
-| 编辑时记录不存在 | `{"code": 1, "msg": "数据不存在"}` |
+| `tab_id` 已被其他分类关联 | `{"code": 1, "msg": "该Tab已被分类【xxx】关联,请勿重复关联"}` |
 
-**成功响应示例**:
+**成功响应示例**(`data.list` 返回保存后的完整数组):
 
 ```json
 {
   "code": 0,
   "msg": "操作成功",
   "data": {
-    "id": 1,
-    "category_name": "运动鞋",
-    "tab_name": "执行左脚程序",
-    "tab_id": 1
+    "list": [
+      {
+        "category_name": "运动鞋",
+        "tab_name": "执行左脚程序",
+        "tab_id": 1
+      }
+    ]
   }
 }
 ```
@@ -118,18 +123,20 @@
 ### 3. 删除分类关联
 
 - **接口地址**:`POST /delete_category_link`
-- **接口说明**:根据 `id` 删除一条分类与Tab配置的关联记录
-- **请求体**(`ModelGetDeviceConfigDetail`,JSON):
+- **接口说明**:根据数组下标删除一条关联记录,删除成功后同步线上配置
+- **请求体**(`CategoryLinksDelete`,JSON):
 
 | 字段 | 类型 | 必填 | 说明 |
 |---|---|---|---|
-| `id` | int | 是 | 关联记录id |
+| `index` | int | 是 | 分类关联数组下标(从0开始,对应查询接口返回的 `list` 顺序) |
+| `token` | str | 是 | 用户token,用于删除后同步线上配置鉴权 |
 
 **请求示例**:
 
 ```json
 {
-  "id": 1
+  "index": 0,
+  "token": "60db012c9dd228dd0b08bea6e6fc915d"
 }
 ```
 
@@ -137,15 +144,17 @@
 
 | 场景 | 返回 |
 |---|---|
-| 记录不存在 | `{"code": 1, "msg": "数据不存在"}` |
+| 下标越界(<0 或 >= 数组长度) | `{"code": 1, "msg": "数据不存在"}` |
 
-**成功响应示例**:
+**成功响应示例**(`data.list` 返回删除后的完整数组):
 
 ```json
 {
   "code": 0,
   "msg": "删除成功",
-  "data": null
+  "data": {
+    "list": []
+  }
 }
 ```
 

+ 71 - 28
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, CategoryLinks
+from databases import DeviceConfig, SysConfigs, SqlQuery, CRUD, select, DeviceConfigTabs
 from service.run_main import RunMain
 import importlib
 from service.auto_deal_pics.upload_pic import UploadPic
@@ -1058,19 +1058,49 @@ def update_tab_name(params: DeviceConfigTabsReq):
     }
 
 
+CATEGORY_LINKS_KEY = "category_links"
+
+
+def _get_category_links_list(session):
+    """从sys_configs读取category_links配置,返回数组;配置不存在或解析失败返回空数组"""
+    sysConfig = CRUD(SysConfigs)
+    item = sysConfig.read(session, conditions={"key": CATEGORY_LINKS_KEY})
+    if item == None:
+        return []
+    try:
+        links = json.loads(item.value)
+        return links if isinstance(links, list) else []
+    except (json.JSONDecodeError, TypeError):
+        return []
+
+
+def _save_category_links_list(session, links):
+    """将category_links数组写回sys_configs,value为json字符串,格式:[{"category_name":"","tab_name":"","tab_id":123}]"""
+    value = json.dumps(links, ensure_ascii=False)
+    sysConfig = CRUD(SysConfigs)
+    item = sysConfig.read(session, conditions={"key": CATEGORY_LINKS_KEY})
+    if item == None:
+        # 配置项不存在则新增一条
+        config = SysConfigs(key=CATEGORY_LINKS_KEY, value=value)
+        session.add(config)
+        session.commit()
+    else:
+        sysConfig.updateConditions(session, conditions={"key": CATEGORY_LINKS_KEY}, value=value)
+
+
 @app.get("/get_category_links", description="获取分类关联列表")
 def get_category_links():
-    """查询所有分类与Tab配置的关联记录(category_links表)"""
+    """查询所有分类与Tab配置的关联(存储于sys_configs,key=category_links)"""
     session = SqlQuery()
-    linkModel = CRUD(CategoryLinks)
-    links = linkModel.read_all(session, order_by="id", ascending=True)
+    links = _get_category_links_list(session)
     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表中是否存在"""
+    """新增或修改分类与Tab配置的关联;tab_id已被其他分类关联时报错;保存后同步线上配置"""
+    settings.USER_TOKEN = params.token
     category_name = (params.category_name or "").strip()
     if category_name == "":
         return {"code": 1, "msg": "分类名称不能为空", "data": None}
@@ -1081,39 +1111,52 @@ def save_category_link(params: CategoryLinksReq):
     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:
+    links = _get_category_links_list(session)
+    # 定位当前编辑项:按分类名称匹配(分类名称唯一),匹配到走修改,否则走新增
+    edit_index = -1
+    for idx, link in enumerate(links):
+        if link.get("category_name") == category_name:
+            edit_index = idx
+            break
+    # 校验tab_id是否已被其他分类关联,已关联则报错提示
+    for idx, link in enumerate(links):
+        if link.get("tab_id") == params.tab_id and idx != edit_index:
             session.close()
-            return {"code": 1, "msg": "该分类名称已存在,请勿重复添加", "data": None}
-        save_link = linkModel.create(session, obj_in=kwargs)
+            return {
+                "code": 1,
+                "msg": f"该Tab已被分类【{link.get('category_name')}】关联,请勿重复关联",
+                "data": None,
+            }
+    # 同步Tab最新名称,保证tab_name与device_config_tabs表中的mode_name一致
+    new_link = {"category_name": category_name, "tab_name": tab.mode_name, "tab_id": tab.id}
+    if edit_index >= 0:
+        # 走修改逻辑,覆盖同名分类的关联配置
+        links[edit_index] = new_link
     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)
+        # 走新增逻辑,追加到数组末尾
+        links.append(new_link)
+    _save_category_links_list(session, links)
     session.close()
-    return {"code": 0, "msg": "操作成功", "data": save_link}
+    # 与其他sys_configs一致,修改后需要同步到线上
+    settings.sync_sys_configs2Online()
+    return {"code": 0, "msg": "操作成功", "data": {"list": links}}
 
 
 @app.post("/delete_category_link", description="删除分类关联")
-def delete_category_link(params: ModelGetDeviceConfigDetail):
-    """根据id删除一条分类与Tab配置的关联记录"""
+def delete_category_link(params: CategoryLinksDelete):
+    """根据数组下标删除一条分类关联,删除后同步线上配置"""
+    settings.USER_TOKEN = params.token
     session = SqlQuery()
-    linkModel = CRUD(CategoryLinks)
-    model = linkModel.read(session, conditions={"id": params.id})
-    if model == None:
+    links = _get_category_links_list(session)
+    if params.index < 0 or params.index >= len(links):
         session.close()
         return {"code": 1, "msg": "数据不存在", "data": None}
-    linkModel.delete(session, obj_id=params.id)
+    links.pop(params.index)
+    _save_category_links_list(session, links)
     session.close()
-    return {"code": 0, "msg": "删除成功", "data": None}
+    # 与其他sys_configs一致,删除后需要同步到线上
+    settings.sync_sys_configs2Online()
+    return {"code": 0, "msg": "删除成功", "data": {"list": links}}
 
 
 @app.post("/get_device_configs", description="获取可执行程序命令列表")

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

+ 0 - 1
python/model/__init__.py

@@ -2,4 +2,3 @@ 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

+ 0 - 12
python/model/category_links.py

@@ -1,12 +0,0 @@
-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)

+ 10 - 3
python/models.py

@@ -144,11 +144,18 @@ class DeviceConfigTabsReq(BaseModel):
 
 
 class CategoryLinksReq(BaseModel):
-    """分类关联配置-新增或修改"""
+    """分类关联配置-新增或修改(存储于sys_configs,key=category_links)"""
 
-    id: Optional[int] = Field(default=None, description="关联记录id,为空则新增")
-    category_name: str = Field(default="", description="分类名称")
+    category_name: str = Field(default="", description="分类名称,唯一")
     tab_id: int = Field(default=0, description="关联的Tab配置id(device_config_tabs表)")
+    token: str = Field(default=None, description="用户token")
+
+
+class CategoryLinksDelete(BaseModel):
+    """分类关联配置-删除"""
+
+    index: int = Field(default=-1, description="分类关联数组下标")
+    token: str = Field(default=None, description="用户token")
 
 
 class RecordUpdate(BaseModel):

+ 2 - 1
python/settings.py

@@ -88,7 +88,8 @@ def sync_sys_configs2Online():
     )
     # 同步本地到线上
     url = DOMAIN + "/api/ai_image/camera_machine/update_all_user_configs"
-    requests.post(url=url, headers=headers, data=data_json)
+    res = requests.post(url=url, headers=headers, data=data_json)
+    print("res", res.content)
     session.close()