2 Commits 9af1639fdc ... e1b475edad

Author SHA1 Message Date
  jiapu e1b475edad 删除群组成员 3 months ago
  jiapu 07bae662ec 新增和修改一些群组接口 3 months ago

+ 571 - 0
code/app对接接口说明.md

@@ -22,6 +22,10 @@
 | 7 | 查询用户自定义方案详情 | `GET` | `/api/app/custom-plan/{id}` | 查询当前用户自己的某个自定义方案详情,校验方案归属,含穴位步骤列表 |
 | 8 | 修改用户自定义方案 | `PUT` | `/api/app/custom-plan` | 当前用户修改自己的自定义方案(含穴位步骤),仅允许修改创作人为当前用户、模式为自定义模式的方案 |
 | 9 | 删除用户自定义方案 | `DELETE` | `/api/app/custom-plan/{id}` | 当前用户删除自己的自定义方案,仅允许删除创作人为当前用户、模式为自定义模式的方案 |
+| 10 | 查询群组成员列表 | `GET` | `/api/app/group/{groupId}/members` | 查询指定家庭设备群组下的成员列表,仅返回当前 APP 账号所属群组的成员 |
+| 11 | 新增群组成员 | `POST` | `/api/app/group/{groupId}/members` | 当前 APP 账号向指定家庭设备群组添加新成员(包含用户档案信息),需校验群组归属 |
+| 12 | 修改群组成员 | `PUT` | `/api/app/group/{groupId}/members/{profileId}` | 当前 APP 账号修改指定群组下已有成员的用户档案信息,需校验群组归属 |
+| 13 | 删除群组成员 | `DELETE` | `/api/app/group/{groupId}/members/{profileId}` | 当前 APP 账号从指定家庭设备群组中移除成员(软删除),需校验群组归属且禁止删除自身 |
 ## 3. 接口详情
 
 ### 3.1 查询用户背部产品穴位坐标列表
@@ -1001,4 +1005,571 @@ curl -X DELETE "http://localhost:18888/api/app/custom-plan/2067138786800111618"
 - 仅允许删除创作人为当前用户的自定义模式方案,不存在或不属于当前用户的方案均返回 `1700` 错误。
 - 删除方案时会同步删除该方案下的所有穴位步骤。
 
+### 3.10 查询群组成员列表
+
+#### 3.10.1 接口用途
+
+用于 APP 端查询指定家庭设备群组下的所有成员列表。
+
+后端会校验当前 APP 账号是否属于该家庭设备群组(`device_group_member.app_user_id = 当前账号ID`),仅返回当前账号所属群组的成员,越权或群组不存在时返回 `403` 错误。
+
+#### 3.10.2 请求信息
+
+- 请求方式:`GET`
+- URI:`/api/app/group/{groupId}/members`
+- 完整示例:`http://localhost:18888/api/app/group/2001/members`
+
+#### 3.10.3 请求参数
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `groupId` | `number` | 是 | 家庭设备群组 ID,对应 `device_group.id`,通过路径参数传递 | `2001` |
+
+#### 3.10.4 调用示例
+
+```http
+GET /api/app/group/2001/members HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+```
+
+```bash
+curl -X GET "http://localhost:18888/api/app/group/2001/members" \
+  -H "Authorization: Bearer <token>"
+```
+
+#### 3.10.5 返回示例
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": [
+    {
+      "memberId": 3001,
+      "groupId": 2001,
+      "profileId": 123,
+      "appUserId": 17320687382,
+      "status": 1,
+      "joinTime": "2026-06-01 09:00:00",
+      "profileComplete": true,
+      "name": "张三",
+      "gender": 1,
+      "age": 35,
+      "phone": "13800138000",
+      "provinceCode": "440000",
+      "cityCode": "440300",
+      "districtCode": "440305",
+      "address": "广东省深圳市南山区",
+      "shoulderWidth": 42.50,
+      "bodyHeight": 85.00,
+      "spineLength": 48.00,
+      "height": 175.00,
+      "weight": 70.00,
+      "fingerWidth1": 2.20,
+      "fingerWidth15": 3.20,
+      "fingerWidth3": 6.50,
+      "acupointTableId": 5001,
+      "profile": {
+        "profileId": 123,
+        "id": 123,
+        "appUserId": 17320687382,
+        "name": "张三",
+        "gender": 1,
+        "age": 35,
+        "phone": "13800138000",
+        "provinceCode": "440000",
+        "cityCode": "440300",
+        "districtCode": "440305",
+        "address": "广东省深圳市南山区",
+        "shoulderWidth": 42.50,
+        "bodyHeight": 85.00,
+        "spineLength": 48.00,
+        "height": 175.00,
+        "weight": 70.00,
+        "fingerWidth1": 2.20,
+        "fingerWidth15": 3.20,
+        "fingerWidth3": 6.50,
+        "acupointTableId": 5001,
+        "profileComplete": true
+      }
+    }
+  ]
+}
+```
+
+#### 3.10.6 字段说明
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| `memberId` | `number` | 群组成员记录 ID,对应 `device_group_member.id` |
+| `groupId` | `number` | 所属群组 ID,对应 `device_group.id` |
+| `profileId` | `number` | 关联的用户档案 ID,对应 `user_profile.id` |
+| `appUserId` | `number/null` | 关联的 APP 账号 ID,对应 `app_user.id`,未关联账号时为 `null` |
+| `status` | `number` | 成员状态:`1=正常`,`0=已移除` |
+| `joinTime` | `string` | 加入时间 |
+| `profileComplete` | `boolean` | 用户档案是否已补全设备使用所需资料 |
+| `name` | `string` | 姓名 |
+| `gender` | `number` | 性别:`1=男`,`2=女` |
+| `age` | `number` | 年龄 |
+| `phone` | `string` | 手机号 |
+| `provinceCode` | `string` | 省级行政区划编码 |
+| `cityCode` | `string` | 市级行政区划编码 |
+| `districtCode` | `string` | 区/县级行政区划编码 |
+| `address` | `string` | 详细地址 |
+| `shoulderWidth` | `number` | 肩宽(cm) |
+| `bodyHeight` | `number` | 身长/坐高(cm) |
+| `spineLength` | `number` | C7-S4 脊柱长度(cm) |
+| `height` | `number` | 身高(cm) |
+| `weight` | `number` | 体重(kg) |
+| `fingerWidth1` | `number` | 一寸指宽(cm) |
+| `fingerWidth15` | `number` | 1.5 寸指宽(cm) |
+| `fingerWidth3` | `number` | 三寸指宽(cm) |
+| `acupointTableId` | `number/null` | 已生成的穴位对照表 ID,对应 `user_acupoint.id` |
+| `userCategoryId` | `number/null` | 用户分类 ID |
+| `profile` | `object` | 嵌套的完整用户档案对象,字段同上 |
+
+#### 3.10.7 失败返回示例
+
+群组不存在或当前账号无权访问:
+
+```json
+{
+  "code": 403,
+  "message": "无权访问该家庭群组",
+  "data": null
+}
+```
+
+#### 3.10.8 备注
+
+- 越权校验在 Service 层完成,仅当 `device_group_member.app_user_id = 当前账号ID` 且 `status = 1` 时才返回成员列表。
+- 本接口不返回分页结构,按 `joinTime` 升序返回所有启用成员。
+- 响应中同时包含扁平化的成员字段和嵌套的 `profile` 对象,内容一致。
+- 该接口只服务于家庭设备(`deviceType=1`)对应的群组,公共设备(`deviceType=2`)没有群组概念。
+- 不会返回 `status=0`(已移除)的成员记录。
+
+### 3.11 新增群组成员
+
+#### 3.11.1 接口用途
+
+用于 APP 端让当前登录账号向指定的家庭设备群组添加新成员。
+
+后端校验:
+- 当前 APP 账号必须属于该家庭设备群组(`device_group_member.app_user_id = 当前账号ID`)
+- 同一群组内手机号不可重复
+- 同一群组内同一 APP 账号不可重复
+- 指宽数据需符合性别对应的合理范围
+
+新增成员时:
+- 若手机号已注册 APP 账号,自动关联已有 APP 账号,并复用其用户档案或在该账号下创建新档案
+- 若手机号未注册,自动创建新 APP 账号和用户档案
+- 自动维护 `user_device` 绑定记录和 `user_acupoint` 穴位对照表(档案完整时)
+
+#### 3.11.2 请求信息
+
+- 请求方式:`POST`
+- URI:`/api/app/group/{groupId}/members`
+- 完整示例:`http://localhost:18888/api/app/group/2001/members`
+
+#### 3.11.3 请求参数
+
+请求体格式:`application/json`
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `name` | `string` | 是 | 姓名 | `李四` |
+| `gender` | `number` | 否 | 性别:`1=男`,`2=女` | `1` |
+| `age` | `number` | 否 | 年龄 | `30` |
+| `phone` | `string` | 否 | 手机号(11 位,1[3-9] 开头);未传时不会关联 APP 账号 | `13900139000` |
+| `userCategoryId` | `number` | 否 | 用户分类 ID,对应 `user_category.id` | `1` |
+| `provinceCode` | `string` | 否 | 省级行政区划编码 | `440000` |
+| `cityCode` | `string` | 否 | 市级行政区划编码 | `440300` |
+| `districtCode` | `string` | 否 | 区/县级行政区划编码 | `440305` |
+| `address` | `string` | 否 | 详细地址 | `广东省深圳市南山区` |
+| `shoulderWidth` | `number` | 否 | 肩宽(cm) | `42.50` |
+| `bodyHeight` | `number` | 否 | 身长/坐高(cm) | `85.00` |
+| `spineLength` | `number` | 否 | C7-S4 脊柱长度(cm) | `48.00` |
+| `height` | `number` | 否 | 身高(cm) | `175.00` |
+| `weight` | `number` | 否 | 体重(kg) | `70.00` |
+| `fingerWidth1` | `number` | 否 | 一寸指宽(cm),男 1.5~2.8,女 1.3~2.4 | `2.20` |
+| `fingerWidth15` | `number` | 否 | 1.5 寸指宽(cm),男 2.2~4.0,女 2.0~3.5 | `3.20` |
+| `fingerWidth3` | `number` | 否 | 三寸指宽(cm),男 4.5~8.0,女 4.0~7.0 | `6.50` |
+| `acupointTableId` | `number` | 否 | 已存在的穴位对照表 ID,一般由后端自动生成,无需传入 | `null` |
+
+#### 3.11.4 调用示例
+
+```http
+POST /api/app/group/2001/members HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+Content-Type: application/json
+
+{
+  "name": "李四",
+  "gender": 1,
+  "age": 30,
+  "phone": "13900139000",
+  "provinceCode": "440000",
+  "cityCode": "440300",
+  "districtCode": "440305",
+  "address": "广东省深圳市南山区",
+  "shoulderWidth": 42.50,
+  "bodyHeight": 85.00,
+  "spineLength": 48.00,
+  "height": 175.00,
+  "weight": 70.00,
+  "fingerWidth1": 2.20,
+  "fingerWidth15": 3.20,
+  "fingerWidth3": 6.50
+}
+```
+
+```bash
+curl -X POST "http://localhost:18888/api/app/group/2001/members" \
+  -H "Authorization: Bearer <token>" \
+  -H "Content-Type: application/json" \
+  -d '{"name":"李四","gender":1,"age":30,"phone":"13900139000","provinceCode":"440000","cityCode":"440300","districtCode":"440305","address":"广东省深圳市南山区","shoulderWidth":42.50,"bodyHeight":85.00,"spineLength":48.00,"height":175.00,"weight":70.00,"fingerWidth1":2.20,"fingerWidth15":3.20,"fingerWidth3":6.50}'
+```
+
+#### 3.11.5 返回示例
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": {
+    "memberId": 3002,
+    "groupId": 2001,
+    "profileId": 124,
+    "appUserId": 17320687383,
+    "status": 1,
+    "joinTime": "2026-06-22 10:30:00",
+    "profileComplete": true,
+    "name": "李四",
+    "gender": 1,
+    "age": 30,
+    "phone": "13900139000",
+    "provinceCode": "440000",
+    "cityCode": "440300",
+    "districtCode": "440305",
+    "address": "广东省深圳市南山区",
+    "shoulderWidth": 42.50,
+    "bodyHeight": 85.00,
+    "spineLength": 48.00,
+    "height": 175.00,
+    "weight": 70.00,
+    "fingerWidth1": 2.20,
+    "fingerWidth15": 3.20,
+    "fingerWidth3": 6.50,
+    "acupointTableId": 5002,
+    "profileComplete": true
+  }
+}
+```
+
+#### 3.11.6 失败返回示例
+
+群组不存在或当前账号无权访问:
+
+```json
+{
+  "code": 403,
+  "message": "无权访问该家庭群组",
+  "data": null
+}
+```
+
+姓名不能为空:
+
+```json
+{
+  "code": 400,
+  "message": "姓名不能为空",
+  "data": null
+}
+```
+
+手机号格式不正确:
+
+```json
+{
+  "code": 1108,
+  "message": "手机号格式不正确",
+  "data": null
+}
+```
+
+同一群组内手机号重复:
+
+```json
+{
+  "code": 1202,
+  "message": "同一群组内手机号不能重复",
+  "data": null
+}
+```
+
+指宽数据超出性别对应范围:
+
+```json
+{
+  "code": 1203,
+  "message": "1寸指宽应在 1.5~2.8 cm 之间",
+  "data": null
+}
+```
+
+#### 3.11.7 备注
+
+- 越权校验在 Service 层完成,仅当 `device_group_member.app_user_id = 当前账号ID` 且 `status = 1` 时才允许新增。
+- 当 `phone` 已注册时,后端会自动关联已有 APP 账号;当 `phone` 未注册时,后端会自动创建新 APP 账号(`appId` 格式:`AJY` + 8 位数字)。
+- 当 `phone` 对应的 APP 账号已存在用户档案时,会复用该档案并更新字段;否则创建新档案。
+- 同一群组内手机号、同一 APP 账号均不可重复。
+- 当新增成员的用户档案完整(`name`、`gender`、`age`、`shoulderWidth`、`bodyHeight`、`spineLength` 都非空)时,后端会自动生成穴位对照表。
+- 后端会自动维护新增成员与群组对应设备的 `user_device` 绑定记录。
+- `name` 字段必填且不可为空字符串。
+
+### 3.12 修改群组成员
+
+#### 3.12.1 接口用途
+
+用于 APP 端让当前登录账号修改指定家庭设备群组下已有成员的用户档案信息。
+
+后端校验:
+- 当前 APP 账号必须属于该家庭设备群组
+- `profileId` 必须是该群组下的有效成员
+- 手机号、APP 账号在同一群组内不能与其他成员冲突
+- 指宽数据需符合性别对应的合理范围
+
+#### 3.12.2 请求信息
+
+- 请求方式:`PUT`
+- URI:`/api/app/group/{groupId}/members/{profileId}`
+- 完整示例:`http://localhost:18888/api/app/group/2001/members/124`
+
+#### 3.12.3 请求参数
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `groupId` | `number` | 是 | 家庭设备群组 ID,对应 `device_group.id`,通过路径参数传递 | `2001` |
+| `profileId` | `number` | 是 | 成员的用户档案 ID,对应 `user_profile.id`,通过路径参数传递 | `124` |
+
+请求体格式:`application/json`,字段定义与 3.11.3 节"新增群组成员"完全一致,包括 `name`(必填)、`gender`、`age`、`phone`、`userCategoryId`、`address`、`shoulderWidth`、`bodyHeight`、`spineLength`、`height`、`weight`、`fingerWidth1`、`fingerWidth15`、`fingerWidth3`、`acupointTableId` 等字段。
+
+#### 3.12.4 调用示例
+
+```http
+PUT /api/app/group/2001/members/124 HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+Content-Type: application/json
+
+{
+  "name": "李四",
+  "gender": 1,
+  "age": 31,
+  "phone": "13900139000",
+  "shoulderWidth": 42.50,
+  "bodyHeight": 85.00,
+  "spineLength": 48.00,
+  "height": 175.00,
+  "weight": 71.00,
+  "fingerWidth1": 2.20,
+  "fingerWidth15": 3.20,
+  "fingerWidth3": 6.50
+}
+```
+
+```bash
+curl -X PUT "http://localhost:18888/api/app/group/2001/members/124" \
+  -H "Authorization: Bearer <token>" \
+  -H "Content-Type: application/json" \
+  -d '{"name":"李四","gender":1,"age":31,"phone":"13900139000","shoulderWidth":42.50,"bodyHeight":85.00,"spineLength":48.00,"height":175.00,"weight":71.00,"fingerWidth1":2.20,"fingerWidth15":3.20,"fingerWidth3":6.50}'
+```
+
+#### 3.12.5 返回示例
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": {
+    "memberId": 3002,
+    "groupId": 2001,
+    "profileId": 124,
+    "appUserId": 17320687383,
+    "status": 1,
+    "joinTime": "2026-06-22 10:30:00",
+    "profileComplete": true,
+    "name": "李四",
+    "gender": 1,
+    "age": 31,
+    "phone": "13900139000",
+    "shoulderWidth": 42.50,
+    "bodyHeight": 85.00,
+    "spineLength": 48.00,
+    "height": 175.00,
+    "weight": 71.00,
+    "fingerWidth1": 2.20,
+    "fingerWidth15": 3.20,
+    "fingerWidth3": 6.50,
+    "acupointTableId": 5002,
+    "profileComplete": true
+  }
+}
+```
+
+#### 3.12.6 失败返回示例
+
+群组不存在或当前账号无权访问:
+
+```json
+{
+  "code": 403,
+  "message": "无权访问该家庭群组",
+  "data": null
+}
+```
+
+成员不存在:
+
+```json
+{
+  "code": 1200,
+  "message": "子用户不存在",
+  "data": null
+}
+```
+
+手机号格式不正确:
+
+```json
+{
+  "code": 1108,
+  "message": "手机号格式不正确",
+  "data": null
+}
+```
+
+手机号与其他成员冲突:
+
+```json
+{
+  "code": 1202,
+  "message": "同一群组内手机号不能重复",
+  "data": null
+}
+```
+
+指宽数据超出性别对应范围:
+
+```json
+{
+  "code": 1203,
+  "message": "1.5寸指宽应在 2.2~4.0 cm 之间",
+  "data": null
+}
+```
+
+#### 3.12.7 备注
+
+- 越权校验在 Service 层完成,仅当 `device_group_member.app_user_id = 当前账号ID` 且 `status = 1` 时才允许修改。
+- 请求体字段语义与 3.11 节"新增群组成员"完全一致,区别在于"修改"语义:未传字段会以"覆盖式"方式写入数据库(`null` 即清空)。
+- 修改手机号时若该手机号已注册 APP 账号,会自动关联;未注册则保持原关联或自动创建。
+- 修改 `phone` 字段后,会同步更新 `device_group_member.app_user_id`。
+- 当修改后档案完整时,后端会自动重新生成穴位对照表。
+- 后端会自动维护成员与群组对应设备的 `user_device` 绑定记录。
+- `name` 字段必填。
+
+### 3.13 删除群组成员
+
+#### 3.13.1 接口用途
+
+用于 APP 端让当前登录账号从指定的家庭设备群组中移除成员。
+
+后端校验:
+- 当前 APP 账号必须属于该家庭设备群组
+- `profileId` 必须是该群组下的有效成员
+- **禁止删除当前账号自身的成员记录**(避免误操作把自己移除出群组)
+
+删除采用软删除方式:将 `device_group_member.status` 置为 `0`,保留历史数据。
+
+#### 3.13.2 请求信息
+
+- 请求方式:`DELETE`
+- URI:`/api/app/group/{groupId}/members/{profileId}`
+- 完整示例:`http://localhost:18888/api/app/group/2001/members/124`
+
+#### 3.13.3 请求参数
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `groupId` | `number` | 是 | 家庭设备群组 ID,对应 `device_group.id`,通过路径参数传递 | `2001` |
+| `profileId` | `number` | 是 | 待移除成员的用户档案 ID,对应 `user_profile.id`,通过路径参数传递 | `124` |
+
+#### 3.13.4 调用示例
+
+```http
+DELETE /api/app/group/2001/members/124 HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+```
+
+```bash
+curl -X DELETE "http://localhost:18888/api/app/group/2001/members/124" \
+  -H "Authorization: Bearer <token>"
+```
+
+#### 3.13.5 返回示例
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": null
+}
+```
+
+#### 3.13.6 失败返回示例
+
+群组不存在或当前账号无权访问:
+
+```json
+{
+  "code": 403,
+  "message": "无权访问该家庭群组",
+  "data": null
+}
+```
+
+成员不存在:
+
+```json
+{
+  "code": 1200,
+  "message": "子用户不存在",
+  "data": null
+}
+```
+
+尝试删除自身成员记录:
+
+```json
+{
+  "code": 403,
+  "message": "不能移除当前账号在群组中的成员记录",
+  "data": null
+}
+```
+
+#### 3.13.7 备注
+
+- 越权校验在 Service 层完成,仅当 `device_group_member.app_user_id = 当前账号ID` 且 `status = 1` 时才允许删除。
+- 禁止删除 `app_user_id = 当前账号ID` 的成员记录,避免误操作把当前账号从群组中移除。
+- 删除为软删除(`status = 0`),不会物理删除 `device_group_member` 表记录,便于审计和恢复。
+- 删除后该成员将不再出现在"查询群组成员列表"接口(3.10 节)的返回结果中。
+- 本接口不会级联删除用户的 `user_profile` 档案或 `user_device` 绑定记录;如需彻底清理用户档案,请使用其他档案管理接口。
+- 已删除的成员记录若需要重新加入群组,请使用"新增群组成员"接口(3.11 节)重新创建。
+
 

+ 9 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/AppGroupController.java

@@ -7,6 +7,7 @@ import com.aijiuyi.admin.common.enums.OperationType;
 import com.aijiuyi.admin.controller.dto.AppGroupMemberDTO;
 import com.aijiuyi.admin.service.AppDeviceGroupService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -49,4 +50,12 @@ public class AppGroupController {
                                                     @RequestBody @Valid AppGroupMemberDTO dto) {
         return Result.success(appDeviceGroupService.updateGroupMember(RequestContext.getUserId(), groupId, profileId, dto));
     }
+
+    @DeleteMapping("/{groupId}/members/{profileId}")
+    @Log(value = "App删除群组成员", module = "App群组", operationType = OperationType.UPDATE)
+    public Result<Void> deleteMember(@PathVariable Long groupId,
+                                     @PathVariable Long profileId) {
+        appDeviceGroupService.deleteGroupMember(RequestContext.getUserId(), groupId, profileId);
+        return Result.success();
+    }
 }

+ 2 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/AppDeviceGroupService.java

@@ -26,6 +26,8 @@ public interface AppDeviceGroupService {
 
     Map<String, Object> updateGroupMember(Long appUserId, Long groupId, Long profileId, AppGroupMemberDTO dto);
 
+    void deleteGroupMember(Long appUserId, Long groupId, Long profileId);
+
     Map<String, Object> getCurrentProfile(Long appUserId);
 
     Map<String, Object> updateCurrentProfile(Long appUserId, AppProfileDTO dto);

+ 30 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/impl/AppDeviceGroupServiceImpl.java

@@ -256,6 +256,36 @@ public class AppDeviceGroupServiceImpl implements AppDeviceGroupService {
         return buildMemberMap(member, updatedProfile);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteGroupMember(Long appUserId, Long groupId, Long profileId) {
+        DeviceGroup group = assertGroupAccess(groupId, appUserId);
+        DeviceGroupMember member = findActiveMemberByProfileId(group.getId(), profileId);
+        if (member == null) {
+            throw new BusinessException(ResultCode.PROFILE_NOT_FOUND);
+        }
+        // 不允许删除当前账号自己的成员记录
+        if (Objects.equals(member.getAppUserId(), appUserId)) {
+            throw new BusinessException(ResultCode.FORBIDDEN.getCode(), "不能移除当前账号在群组中的成员记录");
+        }
+        DeviceGroupMember update = new DeviceGroupMember();
+        update.setId(member.getId());
+        update.setStatus(0);
+        deviceGroupMemberMapper.updateById(update);
+
+        // 同步解绑 user_device:删除群组成员时,把该 profile 与该 device 的绑定置为已解绑。
+        // 否则管理端"用户列表"通过 LEFT JOIN user_device ud ON ud.status=1 仍能查到 deviceCodes,
+        // 且前端 user/device 页面也会继续显示该用户绑定了该设备,造成"删除不彻底"的错觉。
+        // 仅解绑本群关联的 deviceCode,不影响该 profile 与其他设备的绑定。
+        Device device = requireDevice(group.getDeviceCode());
+        LambdaUpdateWrapper<UserDevice> wrapper = new LambdaUpdateWrapper<UserDevice>()
+                .eq(UserDevice::getUserId, profileId)
+                .eq(UserDevice::getDeviceCode, device.getDeviceCode())
+                .set(UserDevice::getStatus, 0)
+                .set(UserDevice::getIsPrimary, 0);
+        userDeviceMapper.update(null, wrapper);
+    }
+
     @Override
     public Map<String, Object> getCurrentProfile(Long appUserId) {
         AppUser appUser = requireAppUser(appUserId);

+ 6 - 0
code/frontend/src/utils/request.js

@@ -63,11 +63,17 @@ request.interceptors.request.use(
 )
 
 // ==================== 响应拦截器 ====================
+// 后端 Long 已通过 Jackson 序列化为字符串,下面的 convertIdsToString 属于双重保险,
+// 防止极少数后端字段未被 Jackson 覆盖(例如直接走非默认 ObjectMapper 输出的场景),
+// 也能防止接口直接返回 int 类型的 ID 字段时被 JS Number 截断。
 request.interceptors.response.use(
   response => {
     const res = response.data
     // 业务状态码判断(后端统一返回格式:{ code, message, data })
     if (res.code === 200) {
+      if (res.data !== undefined && res.data !== null) {
+        res.data = convertIdsToString(res.data)
+      }
       return res
     }
     // Token 失效或未登录

+ 10 - 1
code/frontend/src/views/user/device/index.vue

@@ -152,7 +152,16 @@ const route = useRoute()
 const router = useRouter()
 
 // 从路由参数获取用户信息(由用户管理页跳转传入)
-const userId = computed(() => route.query.userId ? Number(route.query.userId) : null)
+// 注意:userId 必须保持字符串传递。后端使用雪花算法生成的ID为19位数字,
+// 超过 JavaScript 安全整数范围(2^53-1 ≈ 9e15),若先转 Number() 会精度丢失,
+// 导致后端按丢失后的 userId 查询 user_device 找不到任何数据。
+const userId = computed(() => {
+  const value = route.query.userId
+  if (value === undefined || value === null || value === '') {
+    return null
+  }
+  return String(value)
+})
 const userName = computed(() => route.query.userName || '')
 
 // 列表数据