Quellcode durchsuchen

app用户对自己自定义方案的CRUD

jiapu vor 3 Monaten
Ursprung
Commit
fc2d20fddb

+ 553 - 80
code/app对接接口说明.md

@@ -16,9 +16,12 @@
 | 1 | 查询用户背部产品穴位坐标列表 | `GET` | `/api/user/acupoint/{profileId}/product-back-coordinates` | 查询指定子用户身体部位为背部且产品穴位为是的穴位坐标 |
 | 2 | 查询当前账号下的设备列表 | `GET` | `/api/app/device/list` | 通过 Token 获取当前 APP 账号可使用的设备列表 |
 | 3 | 用户手动输入设备编号绑定设备 | `POST` | `/api/app/user/device/bind-by-code` | APP 端按设备编号绑定到当前 Token 账号下,编号不存在返回错误提示 |
-| 4 | 查询设备详情 | `GET` | `/api/device/{id}` | 后台管理系统查询指定设备的详细信息 |
-| 5 | APP 端查询设备详情 | `GET` | `/api/app/device/{id}` | APP 端查询当前账号可访问的设备详情(仅限已绑定的家庭/公共设备) |
-
+| 4 | APP 端查询设备详情 | `GET` | `/api/app/device/{id}` | APP 端查询当前账号可访问的设备详情(仅限已绑定的家庭/公共设备) |
+| 5 | 新增用户自定义方案 | `POST` | `/api/app/custom-plan` | 当前用户新增一个自定义模式方案(含穴位步骤),后端强制设置 modeType=3、authorType=2、authorId=当前用户 |
+| 6 | 查询当前用户的自定义方案列表 | `GET` | `/api/app/custom-plan/list` | 通过 Token 获取当前 APP 账号下创作人属于自己的自定义模式方案基本信息列表(不含穴位步骤),如需详情请调用第 7 个接口 |
+| 7 | 查询用户自定义方案详情 | `GET` | `/api/app/custom-plan/{id}` | 查询当前用户自己的某个自定义方案详情,校验方案归属,含穴位步骤列表 |
+| 8 | 修改用户自定义方案 | `PUT` | `/api/app/custom-plan` | 当前用户修改自己的自定义方案(含穴位步骤),仅允许修改创作人为当前用户、模式为自定义模式的方案 |
+| 9 | 删除用户自定义方案 | `DELETE` | `/api/app/custom-plan/{id}` | 当前用户删除自己的自定义方案,仅允许删除创作人为当前用户、模式为自定义模式的方案 |
 ## 3. 接口详情
 
 ### 3.1 查询用户背部产品穴位坐标列表
@@ -324,17 +327,21 @@ curl -X POST "http://localhost:18888/api/app/user/device/bind-by-code" \
 - 绑定成功后,绑定记录默认 `status=1`、`isPrimary=0`。
 - APP 可根据失败响应中的 `message` 直接弹窗,例如设备编号不存在时提示"设备不存在"。
 
-### 3.4 查询设备详情
-
+### 3.4 APP 端查询设备详情
 #### 3.4.1 接口用途
 
-用于后台管理系统查询指定设备的详细信息,包括设备基本信息、在线状态、关联用户等。
+用于 APP 端查询当前登录账号可访问的设备详情。
+
+后端会根据 Token 中的 APP 账号 ID 校验访问权限:
+- 家庭设备(`deviceType=1`):校验当前 APP 账号是否在该设备的家庭群组内(`device_group_member`)
+- 公共设备(`deviceType=2`):校验当前 APP 账号下的用户档案是否已绑定该设备(`user_device`)
+- 越权或设备不存在时返回 `403` 或 `1300` 错误
 
 #### 3.4.2 请求信息
 
 - 请求方式:`GET`
-- URI:`/device/{id}`
-- 完整示例:`http://localhost:18888/device/1001`
+- URI:`/api/app/device/{id}`
+- 完整示例:`http://localhost:18888/api/app/device/1001`
 
 #### 3.4.3 请求参数
 
@@ -345,13 +352,13 @@ curl -X POST "http://localhost:18888/api/app/user/device/bind-by-code" \
 #### 3.4.4 调用示例
 
 ```http
-GET /device/1001 HTTP/1.1
+GET /api/app/device/1001 HTTP/1.1
 Host: localhost:18888
 Authorization: Bearer <token>
 ```
 
 ```bash
-curl -X GET "http://localhost:18888/device/1001" \
+curl -X GET "http://localhost:18888/api/app/device/1001" \
   -H "Authorization: Bearer <token>"
 ```
 
@@ -378,9 +385,7 @@ curl -X GET "http://localhost:18888/device/1001" \
     "remark": "测试设备",
     "deleted": 0,
     "createTime": "2026-06-01 09:00:00",
-    "updateTime": "2026-06-02 10:30:00",
-    "boundUserCount": 3,
-    "boundUserNames": "张三、李四、王五"
+    "updateTime": "2026-06-02 10:30:00"
   }
 }
 ```
@@ -390,11 +395,11 @@ curl -X GET "http://localhost:18888/device/1001" \
 | 字段名 | 类型 | 说明 |
 | --- | --- | --- |
 | `id` | `number` | 设备 ID,对应 `device.id` |
-| `deviceCode` | `string` | 设备编号(唯一) |
+| `deviceCode` | `string` | 设备编号 |
 | `deviceName` | `string` | 设备名称 |
-| `deviceModel` | `string` | 设备型号:`AJY-2026` / `AJY-2025` |
+| `deviceModel` | `string` | 设备型号 |
 | `deviceType` | `number` | 设备类型:`1=家庭设备`,`2=公共设备` |
-| `serialNo` | `string` | 设备序列号(唯一) |
+| `serialNo` | `string` | 设备序列号 |
 | `firmwareVersion` | `string` | 固件版本 |
 | `onlineStatus` | `number` | 在线状态:`1=在线`,`0=离线` |
 | `lastOnlineTime` | `string` | 最后在线时间 |
@@ -406,126 +411,594 @@ curl -X GET "http://localhost:18888/device/1001" \
 | `deleted` | `number` | 逻辑删除:`0=未删除`,`1=已删除` |
 | `createTime` | `string` | 创建时间 |
 | `updateTime` | `string` | 更新时间 |
-| `boundUserCount` | `number` | 关联用户/成员数量(非数据库字段,联表聚合) |
-| `boundUserNames` | `string` | 关联用户/成员姓名列表(联表聚合,顿号分隔) |
 
-#### 3.4.7 备注
+#### 3.4.7 失败返回示例
+
+设备不存在:
+
+```json
+{
+  "code": 1300,
+  "message": "设备不存在",
+  "data": null
+}
+```
 
-- 本接口为后台管理系统专用,需要管理员 Token。
-- `boundUserCount`、`boundUserNames` 为联表聚合查询结果,非数据库原生字段。
-- 公共设备返回所有已绑定的用户列表;家庭设备返回该家庭群组的所有成员。
+当前账号无权限访问该设备:
+
+```json
+{
+  "code": 403,
+  "message": "无权访问该设备",
+  "data": null
+}
+```
+
+#### 3.4.8 备注
+
+- 本接口需要 APP 登录后的 Token,不接受后台管理员 Token。
+- 越权校验在 Service 层完成,家庭设备校验 `device_group_member.app_user_id`,公共设备校验 `user_device.user_id` 是否归属当前 APP 账号下的用户档案。
+- 后端不会返回 `boundUserCount`、`boundUserNames`、`userIds` 等聚合字段,避免暴露其他用户信息。
+- 路径参数 `id` 为 `device.id`(主键),不是设备编号 `deviceCode`,APP 调用时需先通过列表接口获取设备主键。
 
-### 3.5 APP 端查询设备详情
+### 3.5 新增用户自定义方案
 
 #### 3.5.1 接口用途
 
-用于 APP 端查询当前登录账号可访问的设备详情。
+用于 APP 端用户新增一个自定义模式方案(含穴位步骤)
 
-后端会根据 Token 中的 APP 账号 ID 校验访问权限:
-- 家庭设备(`deviceType=1`):校验当前 APP 账号是否在该设备的家庭群组内(`device_group_member`)
-- 公共设备(`deviceType=2`):校验当前 APP 账号下的用户档案是否已绑定该设备(`user_device`)
-- 越权或设备不存在时返回 `403` 或 `1300` 错误
+后端强制设置:
+- `modeType` = 3(自定义模式)
+- `authorType` = 2(用户创作)
+- `authorId` = 当前 APP 用户 ID
+- `planCode` = 系统自动生成(S + 6 位随机字符)
+
+前端无需传入这些字段,后端会忽略前端传值。
 
 #### 3.5.2 请求信息
 
-- 请求方式:`GET`
-- URI:`/api/app/device/{id}`
-- 完整示例:`http://localhost:18888/api/app/device/1001`
+- 请求方式:`POST`
+- URI:`/api/app/custom-plan`
+- 完整示例:`http://localhost:18888/api/app/custom-plan`
 
 #### 3.5.3 请求参数
 
+请求体格式:`application/json`
+
 | 参数名 | 类型 | 是否必填 | 说明 | 示例 |
 | --- | --- | --- | --- | --- |
-| `id` | `number` | 是 | 设备 ID,对应 `device.id`,通过路径参数传递 | `1001` |
+| `name` | `string` | 是 | 方案名称(最长 100 字符,不可重复) | `我的祛湿方案` |
+| `symptoms` | `string` | 否 | 适用症状(逗号分隔) | `关节酸痛,乏力` |
+| `applicableGender` | `number` | 否 | 适用人群:`0=不限`,`1=仅男`,`2=仅女` | `0` |
+| `ageMin` | `number` | 否 | 适用年龄最小值(岁) | `18` |
+| `ageMax` | `number` | 否 | 适用年龄最大值(岁) | `60` |
+| `description` | `string` | 否 | 方案描述(最长 500 字符) | `针对湿气重的人群定制` |
+| `status` | `number` | 否 | 状态:`0=草稿`,`1=已发布`。不传默认为 `1` | `1` |
+| `steps` | `array` | 是 | 穴位步骤列表,至少 1 个 | 见下方 `steps` 字段说明 |
+
+`steps` 数组中每个元素字段:
+
+| 参数名 | 类型 | 是否必填 | 说明 |
+| --- | --- | --- | --- |
+| `acupointId` | `number` | 是 | 穴位 ID,对应 `acupoint.id` |
+| `side` | `number` | 否 | 侧别:`1=中心`,`2=左侧`,`3=右侧`,`4=双侧`。不传则跟随穴位默认侧别(一般为 `1`) |
+| `temperature` | `number` | 否 | 温度(°C,范围 35-50) |
+| `duration` | `number` | 否 | 时长(分钟,范围 1-30) |
+| `techniqueId` | `number` | 否 | 艾灸手法 ID(对应 `moxibustion_technique.id`) |
+| `remark` | `string` | 否 | 步骤备注(最长 100 字符) |
+
+> 说明:
+> - 请求体中**无需**传 `id`、`planId`、`stepOrder`、`acupointName`、`techniqueName` 等字段,后端会按 `steps` 数组顺序自动重排 `stepOrder`(从 1 开始),并通过 `acupointId` 自动带出 `acupointName`、`techniqueName`。
+> - 修改接口(`PUT /api/app/custom-plan`)与本接口共用同一份步骤结构,请求示例保持一致。
 
 #### 3.5.4 调用示例
 
 ```http
-GET /api/app/device/1001 HTTP/1.1
+POST /api/app/custom-plan HTTP/1.1
 Host: localhost:18888
 Authorization: Bearer <token>
+Content-Type: application/json
+
+{
+  "name": "我的祛湿方案",
+  "symptoms": "关节酸痛,乏力",
+  "applicableGender": 0,
+  "ageMin": 18,
+  "ageMax": 60,
+  "description": "针对湿气重的人群定制",
+  "status": 1,
+  "steps": [
+    {
+      "acupointId": 371,
+      "side": 1,
+      "temperature": 42.00,
+      "duration": 10,
+      "techniqueId": 1,
+      "remark": "核心穴位"
+    },
+    {
+      "acupointId": 362,
+      "side": 1,
+      "temperature": 42.00,
+      "duration": 10,
+      "techniqueId": 1,
+      "remark": ""
+    }
+  ]
+}
 ```
 
 ```bash
-curl -X GET "http://localhost:18888/api/app/device/1001" \
-  -H "Authorization: Bearer <token>"
+curl -X POST "http://localhost:18888/api/app/custom-plan" \
+  -H "Authorization: Bearer <token>" \
+  -H "Content-Type: application/json" \
+  -d '{"name":"我的祛湿方案","symptoms":"关节酸痛,乏力","applicableGender":0,"ageMin":18,"ageMax":60,"description":"针对湿气重的人群定制","status":1,"steps":[{"acupointId":371,"side":1,"temperature":42.00,"duration":10,"techniqueId":1,"remark":"核心穴位"},{"acupointId":362,"side":1,"temperature":42.00,"duration":10,"techniqueId":1,"remark":""}]}'
 ```
 
 #### 3.5.5 返回示例
 
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": 2067138786800111618
+}
+```
+
+#### 3.5.6 失败返回示例
+
+方案名称重复:
+
+```json
+{
+  "code": 1702,
+  "message": "方案名称已存在",
+  "data": null
+}
+```
+
+步骤为空:
+
+```json
+{
+  "code": 1706,
+  "message": "方案步骤至少保留1个",
+  "data": null
+}
+```
+
+#### 3.5.7 备注
+
+- `modeType`、`effectType`、`planCode`、`authorId`、`authorName`、`authorType` 等字段前端无需传入,后端统一设置。
+- 方案名称不可重复,若重复返回 `1702` 错误。
+- 成功返回 `data` 为新方案的 ID,后续可用于详情查询、修改和删除。
+- `steps` 不能为空,至少包含 1 个穴位步骤。
+
+### 3.6 查询当前用户的自定义方案列表
+
+#### 3.6.1 接口用途
+
+用于 APP 端查询当前登录账号下,自己(作为创作人)创建的所有自定义模式(`modeType=3`)方案的基本信息。
+
+当前场景:APP 中"我的方案"页面展示用户自己创建的自定义方案列表,用于查看、复用或继续编辑。
+
+> 本接口**只返回方案基本信息**,不包含穴位步骤。查看方案详情(含穴位步骤)请使用第 7 个接口 `GET /api/app/custom-plan/{id}`。
+
+#### 3.6.2 请求信息
+
+- 请求方式:`GET`
+- URI:`/api/app/custom-plan/list`
+- 完整示例:`http://localhost:18888/api/app/custom-plan/list`
+
+#### 3.6.3 请求参数
+
+无查询参数。后端通过 `Authorization` 请求头中的 Token 获取当前 APP 账号 ID,仅返回创作人类型为用户(`authorType=2`)、模式为自定义模式(`modeType=3`)且创作人 ID 等于当前账号 ID 的方案。
+
+#### 3.6.4 调用示例
+
+```http
+GET /api/app/custom-plan/list HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+```
+
+```bash
+curl -X GET "http://localhost:18888/api/app/custom-plan/list" \
+  -H "Authorization: Bearer <token>"
+```
+
+#### 3.6.5 返回示例
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": [
+    {
+      "id": 1001,
+      "planCode": "SA1B2C3",
+      "name": "我的颈椎调理方案",
+      "modeType": 3,
+      "effectType": "",
+      "symptoms": "肩颈酸痛,头晕",
+      "applicableGender": 0,
+      "ageMin": 18,
+      "ageMax": 60,
+      "description": "针对长期伏案人群的颈椎调理方案",
+      "authorId": 17320687382,
+      "authorName": "张三",
+      "authorType": 2,
+      "useCount": 12,
+      "avgRating": 4.80,
+      "lastUseTime": "2026-06-15 08:30:00",
+      "status": 1,
+      "createTime": "2026-06-10 09:00:00",
+      "updateTime": "2026-06-12 10:30:00"
+    },
+    {
+      "id": 1002,
+      "planCode": "S9X8Y7Z6",
+      "name": "我的祛湿方案",
+      "modeType": 3,
+      "effectType": "",
+      "symptoms": "关节酸痛,乏力",
+      "applicableGender": 0,
+      "ageMin": 20,
+      "ageMax": 60,
+      "description": "针对湿气重的人群定制",
+      "authorId": 17320687382,
+      "authorName": "张三",
+      "authorType": 2,
+      "useCount": 0,
+      "avgRating": null,
+      "lastUseTime": null,
+      "status": 1,
+      "createTime": "2026-06-13 14:00:00",
+      "updateTime": "2026-06-13 14:00:00"
+    }
+  ]
+}
+```
+
+#### 3.6.6 字段说明
+
+| 字段名 | 类型 | 说明 |
+| --- | --- | --- |
+| `id` | `number` | 方案 ID,对应 `plan.id` |
+| `planCode` | `string` | 方案编码 |
+| `name` | `string` | 方案名称 |
+| `modeType` | `number` | 模式类型,本接口固定返回 `3`(自定义模式) |
+| `effectType` | `string` | 作用类型,自定义模式固定为空字符串 |
+| `symptoms` | `string` | 适用症状(逗号分隔) |
+| `applicableGender` | `number` | 适用人群:`0=不限`,`1=仅男`,`2=仅女` |
+| `ageMin` | `number/null` | 适用年龄最小值(岁) |
+| `ageMax` | `number/null` | 适用年龄最大值(岁) |
+| `description` | `string` | 方案描述 |
+| `authorId` | `number` | 创作人 ID,等于当前 APP 账号 ID |
+| `authorName` | `string` | 创作人名称(冗余字段) |
+| `authorType` | `number` | 创作人类型,本接口固定返回 `2`(用户创作) |
+| `useCount` | `number` | 使用人数 |
+| `avgRating` | `number/null` | 平均评分 |
+| `lastUseTime` | `string/null` | 最后使用时间 |
+| `status` | `number` | 状态:`0=草稿`,`1=已发布`,`2=已下架` |
+| `createTime` | `string` | 创建时间 |
+| `updateTime` | `string` | 更新时间 |
+
+#### 3.6.7 备注
+
+- 本接口**不返回**穴位步骤列表(`steps`),仅返回方案基本信息。如需查看方案详情和穴位步骤,请调用第 7 个接口 `GET /api/app/custom-plan/{id}`。
+- 本接口仅返回当前 APP 账号作为"创作人"的自定义模式方案,不会返回系统方案或其他用户创作的方案。
+- 后端根据 Token 解析当前账号 ID 作为 `authorId`,按 `authorType=2`、`modeType=3`、`authorId=当前用户ID` 严格过滤。
+- `useCount`、`avgRating` 为方案被使用后的统计字段,新建方案的初始值为 `0`/`null`。
+- 结果按 `updateTime` 倒序返回,最近修改或创建的方案排在前。
+
+### 3.7 查询用户自定义方案详情
+
+#### 3.7.1 接口用途
+
+用于 APP 端查询当前登录账号下,自己创建的某个自定义模式方案详情,校验方案归属权,避免越权访问。
+
+#### 3.7.2 请求信息
+
+- 请求方式:`GET`
+- URI:`/api/app/custom-plan/{id}`
+- 完整示例:`http://localhost:18888/api/app/custom-plan/1001`
+
+#### 3.7.3 请求参数
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `id` | `number` | 是 | 方案 ID,对应 `plan.id`,通过路径参数传递 | `1001` |
+
+#### 3.7.4 调用示例
+
+```http
+GET /api/app/custom-plan/1001 HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+```
+
+```bash
+curl -X GET "http://localhost:18888/api/app/custom-plan/1001" \
+  -H "Authorization: Bearer <token>"
+```
+
+#### 3.7.5 返回示例
+
 ```json
 {
   "code": 200,
   "message": "操作成功",
   "data": {
     "id": 1001,
-    "deviceCode": "AJY-0001",
-    "deviceName": "艾灸椅一号",
-    "deviceModel": "AJY-2026",
-    "deviceType": 1,
-    "serialNo": "SN202606020001",
-    "firmwareVersion": "1.0.0",
-    "onlineStatus": 0,
-    "lastOnlineTime": "2026-06-02 10:30:00",
-    "provinceCode": "440000",
-    "cityCode": "440300",
-    "districtCode": "440305",
-    "address": "广东省深圳市南山区",
-    "remark": "测试设备",
-    "deleted": 0,
-    "createTime": "2026-06-01 09:00:00",
-    "updateTime": "2026-06-02 10:30:00"
+    "planCode": "SA1B2C3",
+    "name": "我的颈椎调理方案",
+    "modeType": 3,
+    "symptoms": "肩颈酸痛,头晕",
+    "applicableGender": 0,
+    "ageMin": 18,
+    "ageMax": 60,
+    "description": "针对长期伏案人群的颈椎调理方案",
+    "useCount": 12,
+    "avgRating": 4.80,
+    "status": 1,
+    "createTime": "2026-06-10 09:00:00",
+    "updateTime": "2026-06-12 10:30:00",
+    "steps": [
+      {
+        "id": 5001,
+        "planId": 1001,
+        "stepOrder": 1,
+        "acupointId": 371,
+        "acupointName": "大椎穴",
+        "side": 1,
+        "temperature": 42.00,
+        "duration": 10,
+        "techniqueId": 1,
+        "techniqueName": "温和灸",
+        "remark": ""
+      }
+    ]
   }
 }
 ```
 
-#### 3.5.6 字段说明
+#### 3.7.6 字段说明
+
+方案基本信息字段(`id`、`planCode`、`name`、`modeType`、`effectType`、`symptoms`、`applicableGender`、`ageMin`、`ageMax`、`description`、`authorId`、`authorName`、`authorType`、`useCount`、`avgRating`、`lastUseTime`、`status`、`createTime`、`updateTime`)定义同 3.6.6 节。
+
+`steps` 穴位步骤列表字段:
 
 | 字段名 | 类型 | 说明 |
 | --- | --- | --- |
-| `id` | `number` | 设备 ID,对应 `device.id` |
-| `deviceCode` | `string` | 设备编号 |
-| `deviceName` | `string` | 设备名称 |
-| `deviceModel` | `string` | 设备型号 |
-| `deviceType` | `number` | 设备类型:`1=家庭设备`,`2=公共设备` |
-| `serialNo` | `string` | 设备序列号 |
-| `firmwareVersion` | `string` | 固件版本 |
-| `onlineStatus` | `number` | 在线状态:`1=在线`,`0=离线` |
-| `lastOnlineTime` | `string` | 最后在线时间 |
-| `provinceCode` | `string` | 省级行政区划编码(GB/T 2260) |
-| `cityCode` | `string` | 市级行政区划编码 |
-| `districtCode` | `string` | 区/县级行政区划编码 |
-| `address` | `string` | 详细地址 |
-| `remark` | `string` | 备注 |
-| `deleted` | `number` | 逻辑删除:`0=未删除`,`1=已删除` |
-| `createTime` | `string` | 创建时间 |
-| `updateTime` | `string` | 更新时间 |
+| `steps` | `array` | 穴位步骤列表,按 `stepOrder` 升序 |
+| `steps[].id` | `number` | 步骤 ID,对应 `plan_step.id` |
+| `steps[].planId` | `number` | 所属方案 ID |
+| `steps[].stepOrder` | `number` | 步骤序号(从 1 开始) |
+| `steps[].acupointId` | `number` | 穴位 ID,对应 `acupoint.id` |
+| `steps[].acupointName` | `string` | 穴位名称(冗余字段) |
+| `steps[].side` | `number` | 侧别:`1=中心`,`2=左侧`,`3=右侧`,`4=双侧` |
+| `steps[].temperature` | `number` | 温度(°C,范围 35-50) |
+| `steps[].duration` | `number` | 时长(分钟,范围 1-30) |
+| `steps[].techniqueId` | `number/null` | 艾灸手法 ID |
+| `steps[].techniqueName` | `string/null` | 艾灸手法名称(冗余字段) |
+| `steps[].remark` | `string` | 步骤备注 |
+
+#### 3.7.7 失败返回示例
+
+方案不存在或非当前用户创建的自定义方案:
 
-#### 3.5.7 失败返回示例
+```json
+{
+  "code": 1700,
+  "message": "方案不存在",
+  "data": null
+}
+```
 
-设备不存在:
+#### 3.7.8 备注
+
+- 越权校验在 Service 层完成:仅当 `modeType=3`、`authorType=2`、`authorId=当前用户ID` 同时满足时才返回方案详情,否则按"方案不存在"处理。
+- 不会返回其他用户创作的方案、系统预设方案(`modeType=1/2/4`)的详情。
+- APP 在编辑方案前应先调用本接口获取最新数据。
+
+### 3.8 修改用户自定义方案
+
+#### 3.8.1 接口用途
+
+用于 APP 端让用户修改自己创建的自定义模式方案,包括方案基本信息和穴位步骤列表。
+
+请求体格式与"新增用户自定义方案"接口(`POST /api/app/custom-plan`,见 3.5 节)保持一致,仅多一个 `id` 字段。`steps` 结构、字段含义、必填/可选规则完全相同。
+
+#### 3.8.2 请求信息
+
+- 请求方式:`PUT`
+- URI:`/api/app/custom-plan`
+- 完整示例:`http://localhost:18888/api/app/custom-plan`
+
+#### 3.8.3 请求参数
+
+请求体格式:`application/json`
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `id` | `number` | 是 | 方案 ID,对应 `plan.id` | `1001` |
+| `name` | `string` | 是 | 方案名称(最长 100 字符,不可重复) | `我的祛湿方案` |
+| `symptoms` | `string` | 否 | 适用症状(逗号分隔) | `关节酸痛,乏力` |
+| `applicableGender` | `number` | 否 | 适用人群:`0=不限`,`1=仅男`,`2=仅女` | `0` |
+| `ageMin` | `number` | 否 | 适用年龄最小值(岁) | `18` |
+| `ageMax` | `number` | 否 | 适用年龄最大值(岁) | `60` |
+| `description` | `string` | 否 | 方案描述(最长 500 字符) | `针对湿气重的人群定制` |
+| `status` | `number` | 否 | 状态:`0=草稿`,`1=已发布`,`2=已下架` | `1` |
+| `steps` | `array` | 是 | 穴位步骤列表,传 `[]` 表示清空所有步骤,传 `null` 表示保留原步骤 | 见下方 `steps` 字段说明 |
+
+`steps` 数组中每个元素字段(与 3.5 节"新增用户自定义方案"完全一致):
+
+| 参数名 | 类型 | 是否必填 | 说明 |
+| --- | --- | --- | --- |
+| `acupointId` | `number` | 是 | 穴位 ID,对应 `acupoint.id` |
+| `side` | `number` | 否 | 侧别:`1=中心`,`2=左侧`,`3=右侧`,`4=双侧`。不传则跟随穴位默认侧别(一般为 `1`) |
+| `temperature` | `number` | 否 | 温度(°C,范围 35-50) |
+| `duration` | `number` | 否 | 时长(分钟,范围 1-30) |
+| `techniqueId` | `number` | 否 | 艾灸手法 ID(对应 `moxibustion_technique.id`) |
+| `remark` | `string` | 否 | 步骤备注(最长 100 字符) |
+
+> 说明:请求体中**无需**传 `id`(步骤)、`planId`、`stepOrder`、`acupointName`、`techniqueName` 等字段,后端会按 `steps` 数组顺序自动重排 `stepOrder`(从 1 开始),并通过 `acupointId` 自动带出 `acupointName`、`techniqueName`。
+
+#### 3.8.4 调用示例
+
+```http
+PUT /api/app/custom-plan HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+Content-Type: application/json
+
+{
+  "id": 1001,
+  "name": "我的祛湿方案",
+  "symptoms": "关节酸痛,乏力",
+  "applicableGender": 0,
+  "ageMin": 18,
+  "ageMax": 60,
+  "description": "针对湿气重的人群定制",
+  "status": 1,
+  "steps": [
+    {
+      "acupointId": 371,
+      "side": 1,
+      "temperature": 42.00,
+      "duration": 10,
+      "techniqueId": 1,
+      "remark": "核心穴位"
+    },
+    {
+      "acupointId": 362,
+      "side": 1,
+      "temperature": 42.00,
+      "duration": 10,
+      "techniqueId": 1,
+      "remark": ""
+    }
+  ]
+}
+```
+
+```bash
+curl -X PUT "http://localhost:18888/api/app/custom-plan" \
+  -H "Authorization: Bearer <token>" \
+  -H "Content-Type: application/json" \
+  -d '{"id":1001,"name":"我的祛湿方案","symptoms":"关节酸痛,乏力","applicableGender":0,"ageMin":18,"ageMax":60,"description":"针对湿气重的人群定制","status":1,"steps":[{"acupointId":371,"side":1,"temperature":42.00,"duration":10,"techniqueId":1,"remark":"核心穴位"},{"acupointId":362,"side":1,"temperature":42.00,"duration":10,"techniqueId":1,"remark":""}]}'
+```
+
+#### 3.8.5 成功返回示例
 
 ```json
 {
-  "code": 1300,
-  "message": "设备不存在",
+  "code": 200,
+  "message": "操作成功",
   "data": null
 }
 ```
 
-当前账号无权限访问该设备:
+#### 3.8.6 失败返回示例
+
+方案不存在或非当前用户创建的自定义方案:
 
 ```json
 {
-  "code": 403,
-  "message": "无权访问该设备",
+  "code": 1700,
+  "message": "方案不存在",
+  "data": null
+}
+```
+
+方案名称已存在:
+
+```json
+{
+  "code": 1702,
+  "message": "方案名称已存在",
   "data": null
 }
 ```
 
-#### 3.5.8 备注
+穴位不存在或已停用:
+
+```json
+{
+  "code": 1400,
+  "message": "穴位不存在",
+  "data": null
+}
+```
+
+#### 3.8.7 备注
 
 - 本接口需要 APP 登录后的 Token,不接受后台管理员 Token。
-- 越权校验在 Service 层完成,家庭设备校验 `device_group_member.app_user_id`,公共设备校验 `user_device.user_id` 是否归属当前 APP 账号下的用户档案。
-- 后端不会返回 `boundUserCount`、`boundUserNames`、`userIds` 等聚合字段,避免暴露其他用户信息。
-- 路径参数 `id` 为 `device.id`(主键),不是设备编号 `deviceCode`,APP 调用时需先通过列表接口获取设备主键。
+- 后端会自动校验 `modeType=3`、`authorType=2`、`authorId=当前用户ID`,不满足时按"方案不存在"处理,避免越权。
+- 修改方案时后端使用"先删后增"的方式更新穴位步骤:`steps` 传 `null` 时保留原步骤;传 `[]` 时清空所有步骤;传非空数组时整体替换为新步骤并重新计算步骤序号。
+- 方案名称全局唯一,修改时不能与其他方案(包含其他用户创建的方案)重名。
+- 穴位必须处于启用状态(`acupoint.status=1`)才能被引用。
+- `modeType`、`effectType`、`planCode`、`authorId`、`authorName`、`authorType`、`useCount`、`avgRating` 等字段为系统字段,APP 传入也会被忽略,不允许用户修改。
+### 3.9 删除用户自定义方案
+
+#### 3.9.1 接口用途
+
+用于 APP 端用户删除自己的自定义方案。
+
+后端校验方案归属(`modeType=3`、`authorType=2`、`authorId=当前用户ID`),仅允许删除自己的自定义方案。删除为物理删除,会同步删除方案下的穴位步骤。
+
+#### 3.9.2 请求信息
+
+- 请求方式:`DELETE`
+- URI:`/api/app/custom-plan/{id}`
+- 完整示例:`http://localhost:18888/api/app/custom-plan/2067138786800111618`
+
+#### 3.9.3 请求参数
+
+| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
+| --- | --- | --- | --- | --- |
+| `id` | `number` | 是 | 方案 ID,对应 `plan.id`,通过路径参数传递 | `2067138786800111618` |
+
+#### 3.9.4 调用示例
+
+```http
+DELETE /api/app/custom-plan/2067138786800111618 HTTP/1.1
+Host: localhost:18888
+Authorization: Bearer <token>
+```
+
+```bash
+curl -X DELETE "http://localhost:18888/api/app/custom-plan/2067138786800111618" \
+  -H "Authorization: Bearer <token>"
+```
+
+#### 3.9.5 返回示例
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": null
+}
+```
+
+#### 3.9.6 失败返回示例
+
+方案不存在或不属于当前用户:
+
+```json
+{
+  "code": 1700,
+  "message": "方案不存在",
+  "data": null
+}
+```
+
+#### 3.9.7 备注
+
+- 删除为物理删除,不可恢复,请谨慎操作。
+- 仅允许删除创作人为当前用户的自定义模式方案,不存在或不属于当前用户的方案均返回 `1700` 错误。
+- 删除方案时会同步删除该方案下的所有穴位步骤。
+
+

+ 94 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/AppPlanController.java

@@ -0,0 +1,94 @@
+package com.aijiuyi.admin.controller;
+
+import com.aijiuyi.admin.common.annotation.Log;
+import com.aijiuyi.admin.common.context.RequestContext;
+import com.aijiuyi.admin.common.entity.Result;
+import com.aijiuyi.admin.common.enums.OperationType;
+import com.aijiuyi.admin.controller.dto.AppPlanVO;
+import com.aijiuyi.admin.controller.dto.PlanSaveDTO;
+import com.aijiuyi.admin.entity.Plan;
+import com.aijiuyi.admin.service.PlanService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * App端用户自定义方案 Controller
+ * 提供用户对自己自定义模式方案的增删改查接口
+ */
+@RestController
+@RequestMapping("/app/custom-plan")
+public class AppPlanController {
+
+    @Autowired
+    private PlanService planService;
+
+    /**
+     * 新增用户自定义方案(含穴位步骤)
+     *
+     * @param dto 方案信息
+     * @return 方案ID
+     */
+    @PostMapping
+    @Log(value = "新增用户自定义方案", module = "App方案", operationType = OperationType.INSERT)
+    public Result<Long> create(@RequestBody @Valid PlanSaveDTO dto) {
+        Long userId = RequestContext.getUserId();
+        return Result.success(planService.createUserPlan(dto, userId));
+    }
+
+    /**
+     * 查询当前用户的自定义方案列表(仅方案基本信息,不含穴位步骤)
+     * 方案详情请通过 {@link #detail(Long)} 接口获取
+     *
+     * @return 方案列表
+     */
+    @GetMapping("/list")
+    @Log(value = "查询用户自定义方案列表", module = "App方案", operationType = OperationType.QUERY)
+    public Result<List<Plan>> list() {
+        Long userId = RequestContext.getUserId();
+        return Result.success(planService.listUserCustomPlans(userId));
+    }
+
+    /**
+     * 查询用户自定义方案详情(含穴位步骤)
+     *
+     * @param id 方案ID
+     * @return 方案详情
+     */
+    @GetMapping("/{id}")
+    @Log(value = "查询用户自定义方案详情", module = "App方案", operationType = OperationType.QUERY)
+    public Result<AppPlanVO> detail(@PathVariable Long id) {
+        Long userId = RequestContext.getUserId();
+        return Result.success(planService.getUserPlanDetail(id, userId));
+    }
+
+    /**
+     * 修改用户自己的自定义方案(含穴位步骤)
+     *
+     * @param dto 方案信息
+     * @return 操作结果
+     */
+    @PutMapping
+    @Log(value = "修改用户自定义方案", module = "App方案", operationType = OperationType.UPDATE)
+    public Result<Void> update(@RequestBody @Valid PlanSaveDTO dto) {
+        Long userId = RequestContext.getUserId();
+        planService.updateUserPlan(dto, userId);
+        return Result.success();
+    }
+
+    /**
+     * 删除用户自己的自定义方案
+     *
+     * @param id 方案ID
+     * @return 操作结果
+     */
+    @DeleteMapping("/{id}")
+    @Log(value = "删除用户自定义方案", module = "App方案", operationType = OperationType.DELETE)
+    public Result<Void> delete(@PathVariable Long id) {
+        Long userId = RequestContext.getUserId();
+        planService.deleteUserPlan(id, userId);
+        return Result.success();
+    }
+}

+ 60 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppPlanVO.java

@@ -0,0 +1,60 @@
+package com.aijiuyi.admin.controller.dto;
+
+import com.aijiuyi.admin.entity.PlanStep;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * App端方案响应 VO(含方案基本信息和穴位步骤列表)
+ */
+@Data
+public class AppPlanVO {
+
+    /** 方案ID */
+    private Long id;
+
+    /** 方案编码 */
+    private String planCode;
+
+    /** 方案名称 */
+    private String name;
+
+    /** 模式类型:1=一键艾灸,2=专业模式,3=自定义模式,4=延年圣手 */
+    private Integer modeType;
+
+    /** 适用症状 */
+    private String symptoms;
+
+    /** 适用人群:0=不限,1=仅男,2=仅女 */
+    private Integer applicableGender;
+
+    /** 适用年龄最小值 */
+    private Integer ageMin;
+
+    /** 适用年龄最大值 */
+    private Integer ageMax;
+
+    /** 方案描述 */
+    private String description;
+
+    /** 使用人数 */
+    private Integer useCount;
+
+    /** 平均评分 */
+    private BigDecimal avgRating;
+
+    /** 状态:0=草稿,1=已发布,2=已下架 */
+    private Integer status;
+
+    /** 创建时间 */
+    private LocalDateTime createTime;
+
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+
+    /** 穴位步骤列表 */
+    private List<PlanStep> steps;
+}

+ 44 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/PlanService.java

@@ -1,5 +1,6 @@
 package com.aijiuyi.admin.service;
 
+import com.aijiuyi.admin.controller.dto.AppPlanVO;
 import com.aijiuyi.admin.controller.dto.PlanQueryDTO;
 import com.aijiuyi.admin.controller.dto.PlanSaveDTO;
 import com.aijiuyi.admin.entity.Plan;
@@ -94,4 +95,47 @@ public interface PlanService extends IService<Plan> {
      * @return 步骤列表
      */
     List<PlanStep> getStepsByPlanId(Long planId);
+
+    /**
+     * 用户新增自定义方案(含步骤)
+     *
+     * @param dto    方案信息
+     * @param userId 用户ID
+     * @return 方案ID
+     */
+    Long createUserPlan(PlanSaveDTO dto, Long userId);
+
+    /**
+     * 查询用户的自定义模式方案列表(不含穴位步骤,仅返回方案基本信息)
+     * 方案详情请通过 {@link #getUserPlanDetail(Long, Long)} 获取
+     *
+     * @param userId 用户ID
+     * @return 方案列表
+     */
+    List<Plan> listUserCustomPlans(Long userId);
+
+    /**
+     * 查询用户自定义方案详情(含穴位步骤)
+     *
+     * @param planId 方案ID
+     * @param userId 用户ID
+     * @return 方案详情
+     */
+    AppPlanVO getUserPlanDetail(Long planId, Long userId);
+
+    /**
+     * 用户修改自己的自定义方案(含步骤)
+     *
+     * @param dto    方案信息
+     * @param userId 用户ID
+     */
+    void updateUserPlan(PlanSaveDTO dto, Long userId);
+
+    /**
+     * 用户删除自己的自定义方案
+     *
+     * @param planId 方案ID
+     * @param userId 用户ID
+     */
+    void deleteUserPlan(Long planId, Long userId);
 }

+ 152 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/impl/PlanServiceImpl.java

@@ -4,6 +4,7 @@ import com.aijiuyi.admin.common.constant.ResultCode;
 import com.aijiuyi.admin.common.context.RequestContext;
 import com.aijiuyi.admin.common.exception.BusinessException;
 import com.aijiuyi.admin.common.util.LogUtil;
+import com.aijiuyi.admin.controller.dto.AppPlanVO;
 import com.aijiuyi.admin.controller.dto.PlanQueryDTO;
 import com.aijiuyi.admin.controller.dto.PlanSaveDTO;
 import com.aijiuyi.admin.entity.Acupoint;
@@ -430,6 +431,8 @@ public class PlanServiceImpl extends ServiceImpl<PlanMapper, Plan> implements Pl
             step.setStepOrder(i + 1);
             step.setAcupointId(acupoint.getId());
             step.setAcupointName(acupoint.getName());
+            // 侧别跟随穴位,无侧别信息时默认为 1(中心)
+            step.setSide(acupoint.getSide() != null ? acupoint.getSide() : 1);
             planStepMapper.insert(step);
         }
     }
@@ -455,4 +458,153 @@ public class PlanServiceImpl extends ServiceImpl<PlanMapper, Plan> implements Pl
                 .set(UserPlan::getDeleted, 1)
                 .set(UserPlan::getUpdateTime, LocalDateTime.now()));
     }
+
+    /**
+     * 用户新增自定义方案(含步骤),强制设置 modeType=3、authorType=2、authorId=userId
+     *
+     * @param dto    方案信息
+     * @param userId 用户ID
+     * @return 方案ID
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createUserPlan(PlanSaveDTO dto, Long userId) {
+        if (dto.getSteps() == null || dto.getSteps().isEmpty()) {
+            throw new BusinessException(ResultCode.PLAN_STEP_MIN_ERROR);
+        }
+        // 方案名称仅在当前用户范围内做唯一性校验:不同用户可以起相同的方案名(方案 ID 各自不同)
+        if (StringUtils.hasText(dto.getName())) {
+            long nameCount = lambdaQuery()
+                    .eq(Plan::getName, dto.getName())
+                    .eq(Plan::getAuthorId, userId)
+                    .count();
+            if (nameCount > 0) {
+                throw new BusinessException(ResultCode.PLAN_NAME_EXISTS);
+            }
+        }
+        String planCode = "S" + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
+        Plan plan = new Plan();
+        BeanUtils.copyProperties(dto, plan);
+        // 强制设置为自定义模式,创作人为当前用户
+        plan.setModeType(MODE_TYPE_CUSTOM);
+        plan.setPlanCode(planCode);
+        plan.setStatus(dto.getStatus() != null ? dto.getStatus() : 1);
+        plan.setUseCount(0);
+        // 创作人信息根据当前登录用户自动填充(id、name、type)
+        fillCurrentAuthor(plan);
+        // 安全兜底:避免 fillCurrentAuthor 未设置(异常情况)导致 authorId 缺失
+        plan.setAuthorId(userId);
+        // 强制设置 authorType=2(用户创作),与 APP 端"我的自定义方案"过滤条件一致
+        plan.setAuthorType(2);
+        save(plan);
+        saveSteps(plan.getId(), dto.getSteps());
+        LogUtil.info(PlanServiceImpl.class, "用户[{}]新增自定义方案[{}],编码[{}]", userId, plan.getName(), planCode);
+        return plan.getId();
+    }
+
+    /**
+     * 查询用户的自定义模式方案列表(modeType=3,authorType=2,authorId=userId)
+     * 仅返回方案基本信息,不含穴位步骤;如需步骤请调用 getUserPlanDetail
+     *
+     * @param userId 用户ID
+     * @return 方案列表
+     */
+    @Override
+    public List<Plan> listUserCustomPlans(Long userId) {
+        return lambdaQuery()
+                .eq(Plan::getModeType, MODE_TYPE_CUSTOM)
+                .eq(Plan::getAuthorType, 2)
+                .eq(Plan::getAuthorId, userId)
+                .orderByDesc(Plan::getUpdateTime)
+                .list();
+    }
+
+    /**
+     * 查询用户自定义方案详情(含穴位步骤),校验方案归属
+     *
+     * @param planId 方案ID
+     * @param userId 用户ID
+     * @return 方案详情
+     */
+    @Override
+    public AppPlanVO getUserPlanDetail(Long planId, Long userId) {
+        Plan plan = getById(planId);
+        if (plan == null) {
+            throw new BusinessException(ResultCode.PLAN_NOT_FOUND);
+        }
+        if (!Integer.valueOf(MODE_TYPE_CUSTOM).equals(plan.getModeType())
+                || !Integer.valueOf(2).equals(plan.getAuthorType())
+                || !userId.equals(plan.getAuthorId())) {
+            throw new BusinessException(ResultCode.PLAN_NOT_FOUND);
+        }
+        AppPlanVO vo = new AppPlanVO();
+        BeanUtils.copyProperties(plan, vo);
+        vo.setSteps(getStepsByPlanId(planId));
+        return vo;
+    }
+
+    /**
+     * 用户修改自己的自定义方案(含步骤),仅允许修改自己的自定义模式方案
+     *
+     * @param dto    方案信息
+     * @param userId 用户ID
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateUserPlan(PlanSaveDTO dto, Long userId) {
+        Plan plan = getById(dto.getId());
+        if (plan == null) {
+            throw new BusinessException(ResultCode.PLAN_NOT_FOUND);
+        }
+        // 仅允许修改自己的自定义模式方案
+        if (!Integer.valueOf(MODE_TYPE_CUSTOM).equals(plan.getModeType())
+                || !Integer.valueOf(2).equals(plan.getAuthorType())
+                || !userId.equals(plan.getAuthorId())) {
+            throw new BusinessException(ResultCode.PLAN_NOT_FOUND);
+        }
+        // 修改接口不做名称重名校验:方案 ID 已经存在,用户在已有方案上"任意修改",
+        // 不同用户的自定义方案本就允许同名(方案 ID 不同即可区分)
+        // 更新方案基本信息(仅允许修改的可编辑字段)
+        if (StringUtils.hasText(dto.getName())) {
+            plan.setName(dto.getName());
+        }
+        plan.setSymptoms(dto.getSymptoms());
+        plan.setApplicableGender(dto.getApplicableGender());
+        plan.setAgeMin(dto.getAgeMin());
+        plan.setAgeMax(dto.getAgeMax());
+        plan.setDescription(dto.getDescription());
+        plan.setStatus(dto.getStatus() != null ? dto.getStatus() : plan.getStatus());
+        updateById(plan);
+        // 更新步骤(先删后增)
+        if (dto.getSteps() != null) {
+            planStepMapper.delete(new LambdaQueryWrapper<PlanStep>().eq(PlanStep::getPlanId, dto.getId()));
+            if (!dto.getSteps().isEmpty()) {
+                saveSteps(dto.getId(), dto.getSteps());
+            }
+        }
+        LogUtil.info(PlanServiceImpl.class, "用户[{}]修改自定义方案[{}]", userId, dto.getId());
+    }
+
+    /**
+     * 用户删除自己的自定义方案(仅允许删除 modeType=3、authorType=2、authorId=userId 的方案)
+     *
+     * @param planId 方案ID
+     * @param userId 用户ID
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteUserPlan(Long planId, Long userId) {
+        Plan plan = getById(planId);
+        if (plan == null) {
+            throw new BusinessException(ResultCode.PLAN_NOT_FOUND);
+        }
+        if (!Integer.valueOf(MODE_TYPE_CUSTOM).equals(plan.getModeType())
+                || !Integer.valueOf(2).equals(plan.getAuthorType())
+                || !userId.equals(plan.getAuthorId())) {
+            throw new BusinessException(ResultCode.PLAN_NOT_FOUND);
+        }
+        removeById(planId);
+        planStepMapper.delete(new LambdaQueryWrapper<PlanStep>().eq(PlanStep::getPlanId, planId));
+        LogUtil.info(PlanServiceImpl.class, "用户[{}]删除自定义方案[{}]", userId, planId);
+    }
 }

+ 15 - 0
code/backend/src/main/resources/sql/migration_admin_permission.sql

@@ -0,0 +1,15 @@
+-- ==================== 管理员页面权限配置表 ====================
+-- 执行数据库:aijiuyi_admin
+-- 作用:保存非超级管理员可访问的后台前端路由路径列表
+
+CREATE TABLE IF NOT EXISTS `admin_permission` (
+    `id` BIGINT NOT NULL COMMENT '主键ID',
+    `admin_id` BIGINT NOT NULL COMMENT '管理员ID,关联 app_user.id',
+    `route_paths` TEXT NULL COMMENT '可访问前端路由路径 JSON 数组,如 ["/dashboard","/device"]',
+    `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除',
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_admin_permission_admin_id` (`admin_id`),
+    KEY `idx_admin_permission_deleted` (`deleted`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员页面权限配置表';