Jelajahi Sumber

背部产品穴位接口、用户设备列表接口、用户添加设备接口

jiapu 3 bulan lalu
induk
melakukan
17fe27dbb3

+ 239 - 51
code/app对接接口说明.md

@@ -13,52 +13,146 @@
 
 | 序号 | 接口名称 | 方法 | URI | 说明 |
 | --- | --- | --- | --- | --- |
-| 1 | 按身体部位和产品穴位查询穴位分页列表 | `GET` | `/api/acupoint/page` | 按 `bodyPart`、`productAcupoint` 等条件筛选穴位 |
+| 1 | 查询用户背部产品穴位坐标列表 | `GET` | `/api/user/acupoint/{profileId}/product-back-coordinates` | 查询指定子用户身体部位为背部且产品穴位为是的穴位坐标 |
+| 2 | 查询用户拥有的设备列表 | `GET` | `/api/user/device/page` | 按 `userId` 查询用户当前已绑定设备,`status` 固定为 `1` |
+| 3 | 用户手动输入设备编号绑定设备 | `POST` | `/api/app/user/device/bind-by-code` | APP 端按设备编号绑定设备,编号不存在返回错误提示 |
 
 ## 3. 接口详情
 
-### 3.1 按身体部位和产品穴位查询穴位分页列表
+### 3.1 查询用户背部产品穴位坐标列表
 
 #### 3.1.1 接口用途
 
-用于按身体部位和产品穴位标记筛选穴位列表
+用于查询指定子用户的穴位坐标列表,接口固定筛选身体部位为`背部`,且产品穴位为`是`的穴位
 
-当前场景:获取身体部位为`背部`,且为产品穴位的所有穴位
+当前场景:APP 根据用户档案 ID 获取该用户已计算出的背部产品穴位坐标,用于设备推送、坐标展示或调理方案匹配
 
 #### 3.1.2 请求信息
 
 - 请求方式:`GET`
-- URI:`/api/acupoint/page`
-- 完整示例:`http://localhost:18888/api/acupoint/page`
+- URI:`/api/user/acupoint/{profileId}/product-back-coordinates`
+- 完整示例:`http://localhost:18888/api/user/acupoint/123/product-back-coordinates`
 
 #### 3.1.3 请求参数
 
 | 参数名 | 类型 | 是否必填 | 说明 | 示例 |
 | --- | --- | --- | --- | --- |
-| `bodyPart` | `string` | 否 | 身体部位,精确匹配 | `背部` |
-| `productAcupoint` | `number` | 否 | 是否产品穴位:`1=是`,`0=否` | `1` |
-| `pageNum` | `number` | 否 | 页码,默认 `1` | `1` |
-| `pageSize` | `number` | 否 | 每页条数,默认 `10` | `20` |
-| `name` | `string` | 否 | 穴位名称,模糊匹配 | `大椎` |
-| `meridian` | `string` | 否 | 所属经络,精确匹配 | `督脉` |
-| `status` | `number` | 否 | 状态:`1=活跃`,`0=待完善` | `1` |
-| `parseStatus` | `string` | 否 | 解析状态 | `已解析` |
+| `profileId` | `number` | 是 | 子用户 ID,对应 `user_profile.id`,通过路径参数传递 | `123` |
 
 #### 3.1.4 调用示例
 
 ```http
-GET /api/acupoint/page?pageNum=1&pageSize=20&bodyPart=背部&productAcupoint=1 HTTP/1.1
+GET /api/user/acupoint/123/product-back-coordinates HTTP/1.1
 Host: localhost:18888
 Authorization: Bearer <token>
 ```
 
 ```bash
-curl -X GET "http://localhost:18888/api/acupoint/page?pageNum=1&pageSize=20&bodyPart=%E8%83%8C%E9%83%A8&productAcupoint=1" \
+curl -X GET "http://localhost:18888/api/user/acupoint/123/product-back-coordinates" \
   -H "Authorization: Bearer <token>"
 ```
 
 #### 3.1.5 返回示例
 
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": [
+    {
+      "id": 192837465,
+      "userId": 123,
+      "acupointId": 371,
+      "acupointCode": "dumai-14",
+      "acupointName": "大椎",
+      "acupointSide": "center",
+      "bodyPart": "背部",
+      "productAcupoint": 1,
+      "coordinateX": 0.00,
+      "coordinateY": 0.00,
+      "coordinateZ": 0.00,
+      "confidence": "high"
+    },
+    {
+      "id": 192837466,
+      "userId": 123,
+      "acupointId": 362,
+      "acupointCode": "dumai-04",
+      "acupointName": "命门",
+      "acupointSide": "center",
+      "bodyPart": "背部",
+      "productAcupoint": 1,
+      "coordinateX": 0.00,
+      "coordinateY": 245.30,
+      "coordinateZ": 0.00,
+      "confidence": "medium"
+    }
+  ]
+}
+```
+
+#### 3.1.6 字段说明
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| `id` | `number` | 用户穴位坐标记录 ID,对应 `user_acupoint.id` |
+| `userId` | `number` | 子用户 ID,对应 `user_profile.id` |
+| `acupointId` | `number` | 穴位 ID,对应 `acupoint.id` |
+| `acupointCode` | `string` | 穴位编码 |
+| `acupointName` | `string` | 穴位名称 |
+| `acupointSide` | `string` | 坐标侧别:`center=中心`,`left=左侧`,`right=右侧` |
+| `bodyPart` | `string` | 身体部位,本接口固定返回 `背部` |
+| `productAcupoint` | `number` | 是否产品穴位,本接口固定返回 `1` |
+| `coordinateX` | `number` | X 坐标,单位 mm,相对大椎穴中心,向右为正 |
+| `coordinateY` | `number` | Y 坐标,单位 mm,相对大椎穴中心,向下为正 |
+| `coordinateZ` | `number` | Z 坐标,单位 mm,当前体表坐标为 `0` |
+| `confidence` | `string` | 坐标置信度:`high`、`medium`、`low` |
+
+#### 3.1.7 备注
+
+- 本接口不需要传 `bodyPart` 和 `productAcupoint`,后端固定按 `bodyPart=背部`、`productAcupoint=1` 查询。
+- 本接口只返回启用且未删除的穴位坐标,即 `acupoint.status=1` 且 `acupoint.deleted=0`。
+- 如果该子用户尚未生成穴位坐标表,后端会先自动生成用户穴位坐标,再返回查询结果。
+- `profileId` 使用子用户档案 ID,即 `user_profile.id`,不是后台管理员 ID。
+
+### 3.2 查询用户拥有的设备列表
+
+#### 3.2.1 接口用途
+
+用于查询指定用户当前拥有的设备列表。
+
+当前场景:APP 根据用户档案 ID 查询该用户已绑定且未解绑的设备,用于展示用户可使用的设备。
+
+#### 3.2.2 请求信息
+
+- 请求方式:`GET`
+- URI:`/api/user/device/page`
+- 完整示例:`http://localhost:18888/api/user/device/page`
+
+#### 3.2.3 请求参数
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `userId` | `number` | 是 | 用户档案 ID,对应 `user_profile.id` | `123` |
+| `status` | `number` | 是 | 绑定状态,固定传 `1`,表示只查询当前已绑定设备 | `1` |
+| `pageNum` | `number` | 否 | 页码,默认 `1` | `1` |
+| `pageSize` | `number` | 否 | 每页条数,默认 `10` | `10` |
+
+#### 3.2.4 调用示例
+
+```http
+GET /api/user/device/page?userId=123&status=1&pageNum=1&pageSize=10 HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+```
+
+```bash
+curl -X GET "http://localhost:18888/api/user/device/page?userId=123&status=1&pageNum=1&pageSize=10" \
+  -H "Authorization: Bearer <token>"
+```
+
+#### 3.2.5 返回示例
+
 ```json
 {
   "code": 200,
@@ -66,47 +160,141 @@ curl -X GET "http://localhost:18888/api/acupoint/page?pageNum=1&pageSize=20&body
   "data": {
     "records": [
       {
-        "id": 371,
-        "acupointCode": "dumai-14",
-        "name": "大椎",
-        "singleDouble": "单穴",
-        "meridian": "督脉",
-        "bodyPart": "背部",
-        "bodyRegion": "背部",
-        "locationDescription": "在脊柱区,第7颈椎棘突下凹陷中,后正中线上。",
-        "productAcupoint": 1,
-        "parseStatus": "已解析",
-        "status": 1,
-        "createTime": "2026-04-16T15:27:36",
-        "updateTime": "2026-04-16T15:27:36"
-      },
-      {
-        "id": 362,
-        "acupointCode": "dumai-04",
-        "name": "命门",
-        "singleDouble": "单穴",
-        "meridian": "督脉",
-        "bodyPart": "背部",
-        "bodyRegion": "腰部",
-        "locationDescription": "在脊柱区,第2腰椎棘突下凹陷中,后正中线上。",
-        "productAcupoint": 1,
-        "parseStatus": "已解析",
+        "id": 192837465,
+        "userId": 123,
+        "deviceCode": "AJY-0001",
+        "deviceName": "艾灸椅一号",
+        "deviceModel": "AJY-2026",
+        "isPrimary": 1,
+        "onlineStatus": 0,
         "status": 1,
-        "createTime": "2026-04-16T15:27:36",
-        "updateTime": "2026-04-16T15:27:36"
+        "bindTime": "2026-06-02 10:30:00",
+        "createTime": "2026-06-02 10:30:00",
+        "updateTime": "2026-06-02 10:30:00"
       }
     ],
-    "total": 42,
-    "size": 20,
+    "total": 1,
+    "size": 10,
     "current": 1,
-    "pages": 3
+    "pages": 1
+  }
+}
+```
+
+#### 3.2.6 字段说明
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| `id` | `number` | 用户设备绑定记录 ID,对应 `user_device.id` |
+| `userId` | `number` | 用户档案 ID,对应 `user_profile.id` |
+| `deviceCode` | `string` | 设备编号 |
+| `deviceName` | `string` | 设备名称 |
+| `deviceModel` | `string` | 设备型号 |
+| `isPrimary` | `number` | 是否主设备:`1=是`,`0=否` |
+| `onlineStatus` | `number` | 在线状态:`1=在线`,`0=离线` |
+| `status` | `number` | 绑定状态:`1=已绑定`,`0=已解绑` |
+| `bindTime` | `string` | 绑定时间 |
+
+#### 3.2.7 备注
+
+- APP 查询用户拥有的设备列表时,`status` 固定传 `1`,不查询已解绑设备。
+- `userId` 使用用户档案 ID,即 `user_profile.id`,不是后台管理员 ID。
+- 返回数据来自用户设备绑定记录表 `user_device`,用于展示该用户当前已绑定设备。
+
+### 3.3 用户手动输入设备编号绑定设备
+
+#### 3.3.1 接口用途
+
+用于 APP 端让用户手动输入设备编号后绑定设备。
+
+后端会先根据设备编号查询设备主表 `device`:
+
+- 如果设备编号存在,则自动带出设备名称、设备型号等信息,并新增用户设备绑定记录。
+- 如果设备编号不存在,则返回错误码和错误信息,APP 可直接弹窗提示。
+
+#### 3.3.2 请求信息
+
+- 请求方式:`POST`
+- URI:`/api/app/user/device/bind-by-code`
+- 完整示例:`http://localhost:18888/api/app/user/device/bind-by-code`
+
+#### 3.3.3 请求参数
+
+请求体格式:`application/json`
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `userId` | `number` | 是 | 用户档案 ID,对应 `user_profile.id` | `123` |
+| `deviceCode` | `string` | 是 | 用户手动输入的设备编号,对应 `device.device_code` | `AJY-0001` |
+
+#### 3.3.4 调用示例
+
+```http
+POST /api/app/user/device/bind-by-code HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+Content-Type: application/json
+
+{
+  "userId": 123,
+  "deviceCode": "AJY-0001"
+}
+```
+
+```bash
+curl -X POST "http://localhost:18888/api/app/user/device/bind-by-code" \
+  -H "Authorization: Bearer <token>" \
+  -H "Content-Type: application/json" \
+  -d "{\"userId\":123,\"deviceCode\":\"AJY-0001\"}"
+```
+
+#### 3.3.5 成功返回示例
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": {
+    "id": 192837466,
+    "userId": 123,
+    "deviceCode": "AJY-0001",
+    "deviceName": "艾灸椅一号",
+    "deviceModel": "AJY-2026",
+    "isPrimary": 0,
+    "onlineStatus": 0,
+    "status": 1,
+    "bindTime": "2026-06-02 10:35:00",
+    "createTime": "2026-06-02 10:35:00",
+    "updateTime": "2026-06-02 10:35:00"
   }
 }
 ```
 
-#### 3.1.6 备注
+#### 3.3.6 失败返回示例
+
+设备编号不存在:
+
+```json
+{
+  "code": 1300,
+  "message": "设备不存在",
+  "data": null
+}
+```
+
+该用户已绑定此设备:
+
+```json
+{
+  "code": 1304,
+  "message": "该用户已绑定此设备",
+  "data": null
+}
+```
+
+#### 3.3.7 备注
 
-- `bodyPart` 当前是精确匹配,不是模糊搜索。
-- `productAcupoint` 传 `1` 表示只返回产品穴位,传 `0` 表示只返回非产品穴位。
-- 如果只想获取身体部位选项列表,可调用:`GET /api/acupoint/options/body-parts`
-- 如果需要导出身体部位为背部且为产品穴位的全部穴位数据,可调用:`GET /api/acupoint/export?bodyPart=背部&productAcupoint=1`
+- APP 端只需要传 `userId` 和 `deviceCode`,不需要传 `deviceName`、`deviceModel`
+- 后端会用 `deviceCode` 查询设备主表,存在时自动写入 `deviceName`、`deviceModel`
+- 绑定成功后,绑定记录默认 `status=1`、`isPrimary=0`。
+- APP 可根据失败响应中的 `message` 直接弹窗,例如设备编号不存在时提示“设备不存在”。

+ 13 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/UserAcupointController.java

@@ -3,6 +3,7 @@ package com.aijiuyi.admin.controller;
 import com.aijiuyi.admin.common.annotation.Log;
 import com.aijiuyi.admin.common.entity.Result;
 import com.aijiuyi.admin.common.enums.OperationType;
+import com.aijiuyi.admin.controller.dto.UserAcupointCoordinateDTO;
 import com.aijiuyi.admin.entity.UserAcupoint;
 import com.aijiuyi.admin.service.UserAcupointService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -45,4 +46,16 @@ public class UserAcupointController {
     public Result<List<UserAcupoint>> list(@PathVariable Long profileId) {
         return Result.success(userAcupointService.listByUser(profileId));
     }
+
+    /**
+     * 查询指定子用户的背部产品穴位坐标列表
+     *
+     * @param profileId 子用户ID
+     * @return 背部产品穴位坐标列表
+     */
+    @GetMapping("/{profileId}/product-back-coordinates")
+    @Log(value = "查询用户背部产品穴位坐标", module = "用户穴位管理", operationType = OperationType.QUERY)
+    public Result<List<UserAcupointCoordinateDTO>> productBackCoordinates(@PathVariable Long profileId) {
+        return Result.success(userAcupointService.listBackProductCoordinatesByUser(profileId));
+    }
 }

+ 9 - 0
code/backend/src/main/java/com/aijiuyi/admin/mapper/UserAcupointMapper.java

@@ -1,5 +1,6 @@
 package com.aijiuyi.admin.mapper;
 
+import com.aijiuyi.admin.controller.dto.UserAcupointCoordinateDTO;
 import com.aijiuyi.admin.entity.UserAcupoint;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
@@ -21,6 +22,14 @@ public interface UserAcupointMapper extends BaseMapper<UserAcupoint> {
      */
     List<UserAcupoint> listByUserId(@Param("userId") Long userId);
 
+    /**
+     * 查询指定子用户的背部产品穴位坐标列表
+     *
+     * @param userId 子用户ID(user_profile.id)
+     * @return 背部产品穴位坐标列表
+     */
+    List<UserAcupointCoordinateDTO> listBackProductCoordinatesByUserId(@Param("userId") Long userId);
+
     /**
      * 删除指定子用户的所有穴位坐标记录(物理删除,用于重新生成前清空)
      *

+ 10 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/UserAcupointService.java

@@ -1,5 +1,6 @@
 package com.aijiuyi.admin.service;
 
+import com.aijiuyi.admin.controller.dto.UserAcupointCoordinateDTO;
 import com.aijiuyi.admin.entity.UserAcupoint;
 import com.baomidou.mybatisplus.extension.service.IService;
 
@@ -34,4 +35,13 @@ public interface UserAcupointService extends IService<UserAcupoint> {
      * @return 穴位坐标列表
      */
     List<UserAcupoint> listByUser(Long profileId);
+
+    /**
+     * 查询指定子用户的背部产品穴位坐标列表。
+     * 如果用户尚未生成穴位坐标表,会先自动生成。
+     *
+     * @param profileId 子用户ID
+     * @return 背部产品穴位坐标列表
+     */
+    List<UserAcupointCoordinateDTO> listBackProductCoordinatesByUser(Long profileId);
 }

+ 10 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/UserDeviceService.java

@@ -27,6 +27,16 @@ public interface UserDeviceService extends IService<UserDevice> {
      */
     void bindDevice(UserDevice device);
 
+    /**
+     * App 端按设备编号绑定设备。
+     *
+     * @param userId     用户档案ID(user_profile.id)
+     * @param deviceCode 设备编号
+     * @param appUserId  当前登录 App 用户ID(app_user.id)
+     * @return 新增的绑定记录
+     */
+    UserDevice bindAppDeviceByCode(Long userId, String deviceCode, Long appUserId);
+
     /**
      * 解绑设备
      *

+ 13 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/impl/UserAcupointServiceImpl.java

@@ -5,6 +5,7 @@ import com.aijiuyi.admin.common.exception.BusinessException;
 import com.aijiuyi.admin.common.util.AcupointLocationParser;
 import com.aijiuyi.admin.common.util.AcupointYReference;
 import com.aijiuyi.admin.common.util.LogUtil;
+import com.aijiuyi.admin.controller.dto.UserAcupointCoordinateDTO;
 import com.aijiuyi.admin.entity.Acupoint;
 import com.aijiuyi.admin.entity.UserAcupoint;
 import com.aijiuyi.admin.entity.UserProfile;
@@ -155,6 +156,18 @@ public class UserAcupointServiceImpl extends ServiceImpl<UserAcupointMapper, Use
         return baseMapper.listByUserId(profileId);
     }
 
+    @Override
+    public List<UserAcupointCoordinateDTO> listBackProductCoordinatesByUser(Long profileId) {
+        UserProfile profile = userProfileMapper.selectById(profileId);
+        if (profile == null) {
+            throw new BusinessException(ResultCode.PROFILE_NOT_FOUND);
+        }
+        if (profile.getAcupointTableId() == null) {
+            generateForUser(profileId);
+        }
+        return baseMapper.listBackProductCoordinatesByUserId(profileId);
+    }
+
     // ==================== 核心算法(3.0) ====================
 
     /**

+ 64 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/impl/UserDeviceServiceImpl.java

@@ -4,18 +4,24 @@ import com.aijiuyi.admin.common.constant.ResultCode;
 import com.aijiuyi.admin.common.exception.BusinessException;
 import com.aijiuyi.admin.common.util.LogUtil;
 import com.aijiuyi.admin.controller.dto.UserDeviceQueryDTO;
+import com.aijiuyi.admin.entity.Device;
 import com.aijiuyi.admin.entity.UserDevice;
+import com.aijiuyi.admin.entity.UserProfile;
+import com.aijiuyi.admin.mapper.DeviceMapper;
 import com.aijiuyi.admin.mapper.UserDeviceMapper;
+import com.aijiuyi.admin.mapper.UserProfileMapper;
 import com.aijiuyi.admin.service.UserDeviceService;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 /**
@@ -24,6 +30,12 @@ import java.util.List;
 @Service
 public class UserDeviceServiceImpl extends ServiceImpl<UserDeviceMapper, UserDevice> implements UserDeviceService {
 
+    @Autowired
+    private DeviceMapper deviceMapper;
+
+    @Autowired
+    private UserProfileMapper userProfileMapper;
+
     /**
      * 分页查询设备列表
      *
@@ -66,6 +78,58 @@ public class UserDeviceServiceImpl extends ServiceImpl<UserDeviceMapper, UserDev
         LogUtil.info(UserDeviceServiceImpl.class, "子用户[{}]绑定设备[{}]", device.getUserId(), device.getDeviceCode());
     }
 
+    /**
+     * App 端按设备编号绑定设备。
+     *
+     * @param userId     用户档案ID
+     * @param deviceCode 设备编号
+     * @param appUserId  当前登录 App 用户ID
+     * @return 新增的绑定记录
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public UserDevice bindAppDeviceByCode(Long userId, String deviceCode, Long appUserId) {
+        UserProfile profile = userProfileMapper.selectById(userId);
+        if (profile == null) {
+            throw new BusinessException(ResultCode.PROFILE_NOT_FOUND);
+        }
+        if (appUserId != null && !appUserId.equals(profile.getUserId())) {
+            throw new BusinessException(ResultCode.FORBIDDEN);
+        }
+
+        String normalizedDeviceCode = deviceCode.trim();
+        Device device = deviceMapper.selectOne(new LambdaQueryWrapper<Device>()
+                .eq(Device::getDeviceCode, normalizedDeviceCode)
+                .last("LIMIT 1"));
+        if (device == null) {
+            throw new BusinessException(ResultCode.DEVICE_NOT_FOUND);
+        }
+
+        long count = lambdaQuery()
+                .eq(UserDevice::getUserId, userId)
+                .eq(UserDevice::getDeviceCode, normalizedDeviceCode)
+                .eq(UserDevice::getStatus, 1)
+                .count();
+        if (count > 0) {
+            throw new BusinessException(ResultCode.DEVICE_USER_ALREADY_BOUND);
+        }
+
+        UserDevice userDevice = new UserDevice();
+        userDevice.setUserId(userId);
+        userDevice.setDeviceCode(device.getDeviceCode());
+        userDevice.setDeviceName(device.getDeviceName());
+        userDevice.setDeviceModel(device.getDeviceModel());
+        userDevice.setStatus(1);
+        userDevice.setIsPrimary(0);
+        userDevice.setOnlineStatus(device.getOnlineStatus() != null ? device.getOnlineStatus() : 0);
+        userDevice.setBindTime(LocalDateTime.now());
+        save(userDevice);
+
+        LogUtil.info(UserDeviceServiceImpl.class,
+                "App用户[{}]为子用户[{}]绑定设备[{}]", appUserId, userId, device.getDeviceCode());
+        return userDevice;
+    }
+
     /**
      * 解绑设备(设置状态为0)
      *

+ 25 - 0
code/backend/src/main/resources/mapper/UserAcupointMapper.xml

@@ -11,6 +11,31 @@
         ORDER BY acupoint_id, acupoint_side
     </select>
 
+    <!-- 查询指定子用户的背部产品穴位坐标列表 -->
+    <select id="listBackProductCoordinatesByUserId" resultType="com.aijiuyi.admin.controller.dto.UserAcupointCoordinateDTO">
+        SELECT
+            ua.id,
+            ua.user_id,
+            ua.acupoint_id,
+            a.acupoint_code,
+            ua.acupoint_name,
+            ua.acupoint_side,
+            a.body_part,
+            a.product_acupoint,
+            ua.coordinate_x,
+            ua.coordinate_y,
+            ua.coordinate_z,
+            ua.confidence
+        FROM user_acupoint ua
+        INNER JOIN acupoint a ON a.id = ua.acupoint_id
+        WHERE ua.user_id = #{userId}
+          AND a.body_part = '背部'
+          AND a.product_acupoint = 1
+          AND a.status = 1
+          AND a.deleted = 0
+        ORDER BY ua.acupoint_id, ua.acupoint_side
+    </select>
+
     <!-- 删除指定子用户的所有穴位坐标记录(物理删除) -->
     <delete id="deleteByUserId">
         DELETE FROM user_acupoint