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

feat(backend): 同步本地最新代码到仓库 - 更新aijiuyiBackend项目代码,包括新增方案管理、设备同步、用户穴位表等业务模块,重构实体和服务层架构

jiapu 5 месяцев назад
Родитель
Сommit
89958aef0f
100 измененных файлов с 2610 добавлено и 1005 удалено
  1. 144 230
      openspec/aijiuyiBackend/admin-ui/src/config/resources.js
  2. 8 20
      openspec/aijiuyiBackend/admin-ui/src/layouts/AdminLayout.vue
  3. 24 0
      openspec/aijiuyiBackend/admin-ui/src/router/index.js
  4. 22 18
      openspec/aijiuyiBackend/admin-ui/src/views/AdminUsersView.vue
  5. 125 0
      openspec/aijiuyiBackend/admin-ui/src/views/DeviceUsersView.vue
  6. 14 41
      openspec/aijiuyiBackend/admin-ui/src/views/LocalizationPreviewView.vue
  7. 2 2
      openspec/aijiuyiBackend/admin-ui/src/views/Login.vue
  8. 6 6
      openspec/aijiuyiBackend/admin-ui/src/views/OperationLogsView.vue
  9. 136 0
      openspec/aijiuyiBackend/admin-ui/src/views/SchemeDetailView.vue
  10. 753 0
      openspec/aijiuyiBackend/admin-ui/src/views/SchemeSimulationView.vue
  11. 133 0
      openspec/aijiuyiBackend/admin-ui/src/views/UserDevicesView.vue
  12. 149 124
      openspec/aijiuyiBackend/admin-ui/src/views/resource/CrudView.vue
  13. 515 0
      openspec/aijiuyiBackend/data.sql
  14. 6 0
      openspec/aijiuyiBackend/pom.xml
  15. 2 1
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/config/SecurityConfig.java
  16. 12 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/BatchIdsRequest.java
  17. 6 4
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/CreateAdminRequest.java
  18. 0 19
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/LocalizationPreviewRequest.java
  19. 0 2
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/LoginRequest.java
  20. 2 5
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/LoginResponse.java
  21. 49 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/PushDataResponse.java
  22. 18 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/SchemeDetailResponse.java
  23. 4 3
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/UpdatePasswordRequest.java
  24. 17 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/UserModeStorageResponse.java
  25. 0 24
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Account.java
  26. 12 20
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Acupoint.java
  27. 0 15
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AdminRole.java
  28. 0 16
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AlgorithmParameterSet.java
  29. 0 26
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AlgorithmVersionRecord.java
  30. 22 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppDevice.java
  31. 28 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppUser.java
  32. 18 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppUserDevice.java
  33. 3 4
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppUserFingerprint.java
  34. 1 1
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/BaseEntity.java
  35. 0 15
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/BodyRegion.java
  36. 21 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/DeviceHotkeyMapping.java
  37. 0 28
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/DeviceModel.java
  38. 0 21
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Efficacy.java
  39. 0 27
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/EndUser.java
  40. 5 5
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/MoxaProcess.java
  41. 5 3
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/MoxaSessionRecord.java
  42. 27 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/MoxaTechnique.java
  43. 29 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Scheme.java
  44. 24 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SchemeStep.java
  45. 11 8
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SchemeTargetUser.java
  46. 6 4
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SysAdminUser.java
  47. 9 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SysOperationLog.java
  48. 0 16
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Technique.java
  49. 0 15
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/TechniqueComboRule.java
  50. 24 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/UserAcupointItem.java
  51. 25 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/UserAcupointTable.java
  52. 18 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/UserScheme.java
  53. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AccountMapper.java
  54. 0 3
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AcupointMapper.java
  55. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AdminRoleMapper.java
  56. 0 24
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AdminUserMapper.java
  57. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AlgorithmParameterSetMapper.java
  58. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AlgorithmVersionRecordMapper.java
  59. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppDeviceMapper.java
  60. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppUserDeviceMapper.java
  61. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppUserFingerprintMapper.java
  62. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppUserMapper.java
  63. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/BodyRegionMapper.java
  64. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/DeviceHotkeyMappingMapper.java
  65. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/DeviceModelMapper.java
  66. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/EfficacyMapper.java
  67. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/EfficacyVersionMapper.java
  68. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/EndUserMapper.java
  69. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/MoxaProcessMapper.java
  70. 0 3
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/MoxaSessionRecordMapper.java
  71. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/MoxaTechniqueMapper.java
  72. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/OperationLogMapper.java
  73. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SchemeMapper.java
  74. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SchemeStepMapper.java
  75. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SchemeTargetUserMapper.java
  76. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SymptomMapper.java
  77. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SysAdminUserMapper.java
  78. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SysOperationLogMapper.java
  79. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/TechniqueComboRuleMapper.java
  80. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/TechniqueMapper.java
  81. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserAcupointItemMapper.java
  82. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserAcupointTableMapper.java
  83. 0 10
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserFingerprintMapper.java
  84. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserSchemeMapper.java
  85. 8 11
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/security/AdminDetailsServiceImpl.java
  86. 10 20
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/security/AdminUserDetails.java
  87. 12 16
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/security/OperationLogAspect.java
  88. 0 8
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AccountService.java
  89. 12 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AcupointCoordinateService.java
  90. 0 1
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AcupointService.java
  91. 0 12
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AdminUserService.java
  92. 0 8
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AlgoParamService.java
  93. 0 8
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AlgoVerService.java
  94. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppDeviceService.java
  95. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppUserDeviceService.java
  96. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppUserFingerprintService.java
  97. 7 0
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppUserService.java
  98. 0 2
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AuthService.java
  99. 0 8
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/BodyRegionService.java
  100. 0 8
      openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/ComboRuleService.java

+ 144 - 230
openspec/aijiuyiBackend/admin-ui/src/config/resources.js

@@ -1,264 +1,178 @@
-/**
- * 与 BaseMpController 风格一致的资源:GET 分页、GET/:id、POST、PUT/:id、DELETE/:id
- * apiPath 不含 /api 前缀(http 已 baseURL /api)
- */
 export const RESOURCE_MAP = {
-  symptoms: {
-    title: '症状字典',
-    apiPath: '/v1/symptoms',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'code', label: '编码', width: 100 },
-      { prop: 'name', label: '名称', minWidth: 160 },
-      { prop: 'createTime', label: '创建时间', width: 170 },
-    ],
-    formFields: [
-      { prop: 'code', label: '编码', type: 'input', required: true },
-      { prop: 'name', label: '名称', type: 'input', required: true },
-    ],
-  },
-  'body-regions': {
-    title: '身体部位与分区',
-    apiPath: '/v1/body-regions',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'bodyPart', label: '身体部位', width: 120 },
-      { prop: 'regionPartition', label: '部位分区', minWidth: 140 },
-      { prop: 'createTime', label: '创建时间', width: 170 },
-    ],
-    formFields: [
-      { prop: 'bodyPart', label: '身体部位', type: 'input' },
-      { prop: 'regionPartition', label: '部位分区', type: 'input' },
-    ],
-  },
   acupoints: {
-    title: '标准穴位',
-    apiPath: '/v1/acupoints',
-    chairProductFilter: true,
+    title: '穴位管理',
+    apiPath: '/acupoints',
+    searchApiPath: '/acupoints/search',
+    exportApiPath: '/acupoints/export',
+    batchDeleteApiPath: '/acupoints/batch-delete',
     columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'code', label: '编号', width: 120 },
-      { prop: 'name', label: '名称', width: 100 },
-      { prop: 'meridian', label: '经络', minWidth: 140 },
-      { prop: 'chairProduct', label: '艾灸椅', width: 88 },
-      { prop: 'serialNo', label: '序号', width: 72 },
-    ],
-    formFields: [
-      { prop: 'serialNo', label: '序号', type: 'input' },
-      { prop: 'code', label: '编号', type: 'input', required: true },
-      { prop: 'name', label: '名称', type: 'input', required: true },
-      { prop: 'sideType', label: '单/双穴', type: 'input' },
-      { prop: 'meridian', label: '经络', type: 'input' },
-      { prop: 'bodyRegion', label: '身体部位', type: 'input' },
-      { prop: 'regionPartition', label: '部位分区', type: 'input' },
-      { prop: 'locationText', label: '定位描述', type: 'textarea' },
-      { prop: 'relativePositionText', label: '相对位置', type: 'textarea' },
-      { prop: 'indications', label: '主治', type: 'textarea' },
-      { prop: 'symptomCoverage', label: '症状覆盖', type: 'textarea' },
-      { prop: 'acupointMethod', label: '取穴说明', type: 'textarea' },
-      { prop: 'chairProduct', label: '艾灸椅产品(0/1)', type: 'number' },
-      { prop: 'remark', label: '备注', type: 'input' },
-      { prop: 'baseCoordX', label: '基准X', type: 'input' },
-      { prop: 'baseCoordY', label: '基准Y', type: 'input' },
-      { prop: 'baseCoordZ', label: '基准Z', type: 'input' },
-      { prop: 'heightAdaptFormula', label: '身高适配公式', type: 'input' },
-      { prop: 'trajectoryJson', label: '轨迹 JSON', type: 'textarea' },
-      { prop: 'groupTag', label: '分组标签', type: 'input' },
-    ],
-  },
-  efficacies: {
-    title: '功效',
-    apiPath: '/v1/efficacies',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'name', label: '名称', minWidth: 160 },
-      { prop: 'status', label: '状态', width: 100 },
-      { prop: 'currentVersion', label: '版本', width: 120 },
-    ],
-    formFields: [
-      { prop: 'name', label: '功效名称', type: 'input', required: true },
-      { prop: 'summary', label: '简介', type: 'textarea' },
-      { prop: 'iconUrl', label: '图标 URL', type: 'input' },
-      { prop: 'applicableCrowd', label: '适用人群', type: 'textarea' },
-      { prop: 'descriptionText', label: '说明', type: 'textarea' },
-      { prop: 'status', label: '状态', type: 'input' },
-      { prop: 'currentVersion', label: '当前版本号', type: 'input' },
-      { prop: 'prescriptionJson', label: '灸方 JSON', type: 'textarea' },
-    ],
-  },
-  techniques: {
-    title: '手法',
-    apiPath: '/v1/techniques',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
+      { prop: 'id', label: 'ID', width: 80 },
       { prop: 'code', label: '编码', width: 120 },
-      { prop: 'name', label: '名称', minWidth: 140 },
-    ],
-    formFields: [
-      { prop: 'code', label: '编码', type: 'input', required: true },
-      { prop: 'name', label: '名称', type: 'input', required: true },
-      { prop: 'paramsJson', label: '参数 JSON', type: 'textarea' },
-    ],
-  },
-  'technique-combo-rules': {
-    title: '手法组合规则',
-    apiPath: '/v1/technique-combo-rules',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'name', label: '名称', minWidth: 160 },
+      { prop: 'name', label: '名称', width: 100 },
+      { prop: 'meridian', label: '所属经络', width: 140 },
+      { prop: 'efficacy', label: '功效', minWidth: 160 },
+      { prop: 'side', label: '侧别', width: 80 },
+      { prop: 'status', label: '状态', width: 80 },
+      { prop: 'createTime', label: '创建时间', width: 170 },
     ],
     formFields: [
-      { prop: 'name', label: '规则名称', type: 'input', required: true },
-      { prop: 'logicJson', label: '逻辑 JSON', type: 'textarea' },
+      { prop: 'code', label: '穴位编码', type: 'input', required: true },
+      { prop: 'name', label: '穴位名称', type: 'input', required: true },
+      { prop: 'meridian', label: '所属经络', type: 'select', options: ['督脉', '足太阳膀胱经', '手太阳小肠经', '其他'] },
+      { prop: 'efficacy', label: '功效(逗号分隔)', type: 'input' },
+      { prop: 'positioning', label: '定位方式', type: 'select', options: ['relative', 'absolute'] },
+      { prop: 'offsetCun', label: '纵向偏移(寸)', type: 'input' },
+      { prop: 'cunType', label: '指寸类型', type: 'select', options: ['cun1', 'cun1_5', 'cun3'] },
+      { prop: 'side', label: '侧别', type: 'select', options: ['center', 'both', 'left', 'right'] },
+      { prop: 'lateralOffset', label: '横向偏移(寸)', type: 'input' },
+      { prop: 'targetGender', label: '适用性别', type: 'select', options: ['all', 'male', 'female'] },
+      { prop: 'status', label: '状态', type: 'select', options: ['active', 'pending'] },
+      { prop: 'source', label: '来源', type: 'select', options: ['preset', 'expert'] },
+    ],
+    searchFields: [
+      { prop: 'name', label: '穴位名称', type: 'input' },
+      { prop: 'meridian', label: '所属经络', type: 'select', options: ['', '督脉', '足太阳膀胱经', '手太阳小肠经'] },
+      { prop: 'positioning', label: '定位方式', type: 'select', options: ['', 'relative', 'absolute'] },
+      { prop: 'status', label: '状态', type: 'select', options: ['', 'active', 'pending'] },
     ],
   },
-  'device-models': {
-    title: '设备型号',
-    apiPath: '/v1/device-models',
+  'moxa-techniques': {
+    title: '艾灸手法',
+    apiPath: '/moxa-techniques',
+    searchApiPath: '/moxa-techniques/search',
     columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'code', label: '编码', width: 140 },
-      { prop: 'name', label: '名称', minWidth: 140 },
+      { prop: 'id', label: 'ID', width: 80 },
+      { prop: 'code', label: '编码', minWidth: 100 },
+      { prop: 'name', label: '名称', minWidth: 120 },
+      { prop: 'movementPattern', label: '运动模式', minWidth: 120 },
+      { prop: 'tempMin', label: '最低温度', width: 100 },
+      { prop: 'tempMax', label: '最高温度', width: 100 },
+      { prop: 'status', label: '状态', width: 80 },
     ],
     formFields: [
       { prop: 'code', label: '编码', type: 'input', required: true },
       { prop: 'name', label: '名称', type: 'input', required: true },
-      { prop: 'strokeRangeX', label: '行程X', type: 'input' },
-      { prop: 'strokeRangeY', label: '行程Y', type: 'input' },
-      { prop: 'strokeRangeZ', label: '行程Z', type: 'input' },
-      { prop: 'originOffsetX', label: '原点偏移X', type: 'input' },
-      { prop: 'originOffsetY', label: '原点偏移Y', type: 'input' },
-      { prop: 'originOffsetZ', label: '原点偏移Z', type: 'input' },
-      { prop: 'axisDirectionJson', label: '轴向 JSON', type: 'textarea' },
-      { prop: 'safetyDistance', label: '安全距离', type: 'input' },
-      { prop: 'standardHeightCm', label: '标准人身高cm', type: 'input' },
-      { prop: 'standardWeightKg', label: '标准人体重kg', type: 'input' },
-      { prop: 'standardFingerCunCm', label: '标准人指寸cm', type: 'input' },
-    ],
-  },
-  'algorithm-params': {
-    title: '算法参数集',
-    apiPath: '/v1/algorithm/params',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'deviceModelId', label: '设备型号ID', width: 120 },
-      { prop: 'name', label: '名称', minWidth: 140 },
+      { prop: 'isDefault', label: '默认手法', type: 'select', options: [{ label: '否', value: 0 }, { label: '是', value: 1 }] },
+      { prop: 'description', label: '描述', type: 'textarea' },
+      { prop: 'movementPattern', label: '运动模式', type: 'select', options: ['fixed', 'vertical', 'circular', 'point', 'oneclick'], required: true },
+      { prop: 'tempMin', label: '最低温度(°C)', type: 'input' },
+      { prop: 'tempMax', label: '最高温度(°C)', type: 'input' },
+      { prop: 'durationMin', label: '最短时间(秒)', type: 'number' },
+      { prop: 'durationMax', label: '最长时间(秒)', type: 'number' },
+      { prop: 'movementAmplitude', label: '运动幅度(mm)', type: 'input' },
+      { prop: 'movementSpeed', label: '运动速度(mm/s)', type: 'input' },
+      { prop: 'status', label: '状态', type: 'select', options: ['active', 'disabled'] },
+      { prop: 'remark', label: '备注', type: 'textarea' },
     ],
-    formFields: [
-      { prop: 'deviceModelId', label: '设备型号ID', type: 'number', required: true },
+    searchFields: [
       { prop: 'name', label: '名称', type: 'input' },
-      { prop: 'paramsJson', label: '参数 JSON', type: 'textarea', required: true },
+      { prop: 'movementPattern', label: '运动模式', type: 'select', options: ['', 'fixed', 'vertical', 'circular', 'point', 'oneclick'] },
+      { prop: 'status', label: '状态', type: 'select', options: ['', 'active', 'disabled'] },
     ],
   },
-  'efficacy-versions': {
-    title: '功效历史版本',
-    apiPath: '/v1/efficacy-versions',
+  users: {
+    title: '用户管理',
+    apiPath: '/users',
+    searchApiPath: '/users/search',
+    exportApiPath: '/users/export',
+    batchDeleteApiPath: '/users/batch-delete',
     columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'efficacyId', label: '功效ID', width: 100 },
-      { prop: 'versionNo', label: '版本号', width: 120 },
-      { prop: 'createdAt', label: '创建时间', width: 170 },
-    ],
-    formFields: [
-      { prop: 'efficacyId', label: '功效ID', type: 'number', required: true },
-      { prop: 'versionNo', label: '版本号', type: 'input', required: true },
-      { prop: 'snapshotJson', label: '快照 JSON', type: 'textarea', required: true },
-    ],
-  },
-  'algorithm-versions': {
-    title: '算法版本记录',
-    apiPath: '/v1/algorithm/versions',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'versionLabel', label: '版本标签', width: 140 },
-      { prop: 'createdAt', label: '创建时间', width: 170 },
-    ],
-    formFields: [
-      { prop: 'versionLabel', label: '版本标签', type: 'input', required: true },
-      { prop: 'snapshotJson', label: '快照 JSON', type: 'textarea', required: true },
-      { prop: 'changeNote', label: '变更说明', type: 'textarea' },
-    ],
-  },
-  'end-users': {
-    title: 'C端用户档案',
-    apiPath: '/v1/end-users',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'accountId', label: '账号ID', width: 100 },
-      { prop: 'displayName', label: '显示名', width: 120 },
-      { prop: 'heightCm', label: '身高', width: 80 },
-      { prop: 'shoulderWidthCm', label: '肩宽', width: 80 },
-      { prop: 'spineLengthCm', label: '脊柱长', width: 80 },
-      { prop: 'fingerCunCm', label: '指寸', width: 72 },
-    ],
-    formFields: [
-      { prop: 'accountId', label: '账号ID', type: 'number', required: true },
-      { prop: 'displayName', label: '显示名', type: 'input' },
-      { prop: 'gender', label: '性别', type: 'input' },
-      { prop: 'age', label: '年龄', type: 'number' },
-      { prop: 'heightCm', label: '身高cm', type: 'input' },
-      { prop: 'weightKg', label: '体重kg', type: 'input' },
-      { prop: 'shoulderWidthCm', label: '肩宽cm', type: 'input', required: true },
-      { prop: 'spineLengthCm', label: '脊柱长度cm', type: 'input', required: true },
-      { prop: 'fingerCunCm', label: '指寸cm', type: 'input' },
-    ],
-  },
-  accounts: {
-    title: 'C端账号',
-    apiPath: '/v1/accounts',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'phone', label: '手机号', width: 140 },
-      { prop: 'status', label: '状态', width: 100 },
+      { prop: 'id', label: 'ID', width: 80 },
+      { prop: 'name', label: '姓名', minWidth: 100 },
+      { prop: 'gender', label: '性别', width: 70 },
+      { prop: 'age', label: '年龄', width: 70 },
+      { prop: 'phone', label: '手机号', width: 130 },
+      { prop: 'shoulderWidth', label: '肩宽(cm)', width: 90 },
+      { prop: 'bodyLength', label: '身长(cm)', width: 90 },
+      { prop: 'acupointTableStatus', label: '穴位表', minWidth: 100 },
+      { prop: 'createTime', label: '注册时间', width: 170 },
     ],
     formFields: [
+      { prop: 'name', label: '姓名', type: 'input', required: true },
+      { prop: 'gender', label: '性别', type: 'select', options: ['male', 'female'], required: true },
+      { prop: 'age', label: '年龄', type: 'number', required: true },
       { prop: 'phone', label: '手机号', type: 'input', required: true },
-      { prop: 'passwordHash', label: '密码哈希(可选)', type: 'input' },
-      { prop: 'status', label: '状态', type: 'input' },
-    ],
-  },
-  'user-fingerprints': {
-    title: '用户指纹',
-    apiPath: '/v1/user-fingerprints',
-    columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'endUserId', label: '用户ID', width: 100 },
-      { prop: 'fingerprintId', label: '指纹标识', minWidth: 160 },
-    ],
-    formFields: [
-      { prop: 'endUserId', label: '用户ID', type: 'number', required: true },
-      { prop: 'fingerprintId', label: '指纹标识', type: 'input', required: true },
-    ],
+      { prop: 'shoulderWidth', label: '肩宽(cm)', type: 'input', required: true },
+      { prop: 'bodyLength', label: '身长(cm)', type: 'input', required: true },
+      { prop: 'fingerCun1', label: '一寸(cm)', type: 'input' },
+      { prop: 'fingerCun1_5', label: '1.5寸(cm)', type: 'input' },
+      { prop: 'fingerCun3', label: '三寸(cm)', type: 'input' },
+    ],
+    searchFields: [
+      { prop: 'keyword', label: '姓名/手机号', type: 'input' },
+      { prop: 'gender', label: '性别', type: 'select', options: ['', 'male', 'female'] },
+      { prop: 'acupointTableStatus', label: '穴位表状态', type: 'select', options: ['', 'GENERATED', 'NOT_GENERATED'] },
+      { prop: 'deviceCode', label: '设备编号', type: 'input' },
+    ],
+    extraActions: ['generateAcupoint', 'manageDevices'],
   },
-  'moxa-records': {
-    title: '艾灸会话记录',
-    apiPath: '/v1/moxa-records',
+  devices: {
+    title: '设备管理',
+    apiPath: '/devices',
+    searchApiPath: '/devices/search',
+    exportApiPath: '/devices/export',
+    batchDeleteApiPath: '/devices/batch-delete',
     columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'endUserId', label: '用户ID', width: 100 },
-      { prop: 'deviceId', label: '设备', width: 120 },
-      { prop: 'startedAt', label: '开始', width: 170 },
+      { prop: 'id', label: 'ID', width: 80 },
+      { prop: 'deviceCode', label: '设备编号', minWidth: 120 },
+      { prop: 'deviceName', label: '设备名称', minWidth: 140 },
+      { prop: 'model', label: '型号', width: 100 },
+      { prop: 'serialNumber', label: '序列号', minWidth: 140 },
+      { prop: 'firmwareVersion', label: '固件版本', width: 100 },
+      { prop: 'status', label: '状态', width: 80 },
     ],
     formFields: [
-      { prop: 'endUserId', label: '用户ID', type: 'number', required: true },
-      { prop: 'deviceId', label: '设备标识', type: 'input' },
-      { prop: 'startedAt', label: '开始时间', type: 'input' },
-      { prop: 'endedAt', label: '结束时间', type: 'input' },
+      { prop: 'deviceCode', label: '设备编号', type: 'input', required: true },
+      { prop: 'deviceName', label: '设备名称', type: 'input', required: true },
+      { prop: 'model', label: '设备型号', type: 'select', options: ['AJY-2026', 'AJY-2025'], required: true },
+      { prop: 'serialNumber', label: '序列号', type: 'input', required: true },
+      { prop: 'firmwareVersion', label: '固件版本', type: 'input' },
+      { prop: 'status', label: '状态', type: 'select', options: ['online', 'offline'] },
       { prop: 'remark', label: '备注', type: 'textarea' },
     ],
+    searchFields: [
+      { prop: 'deviceCode', label: '设备编号', type: 'input' },
+      { prop: 'deviceName', label: '设备名称', type: 'input' },
+      { prop: 'status', label: '在线状态', type: 'select', options: ['', 'online', 'offline'] },
+      { prop: 'userName', label: '绑定用户', type: 'input' },
+    ],
+    extraActions: ['manageUsers'],
   },
-  'admin-roles': {
-    title: '后台角色',
-    apiPath: '/v1/admin/roles',
+  schemes: {
+    title: '方案管理',
+    apiPath: '/schemes',
+    searchApiPath: '/schemes/search',
+    batchDeleteApiPath: '/schemes/batch-delete',
     columns: [
-      { prop: 'id', label: 'ID', width: 72 },
-      { prop: 'code', label: '编码', width: 140 },
-      { prop: 'name', label: '名称', minWidth: 120 },
+      { prop: 'id', label: 'ID', width: 80 },
+      { prop: 'schemeName', label: '方案名称', minWidth: 140 },
+      { prop: 'mode', label: '模式', width: 120 },
+      { prop: 'efficacyType', label: '功效类型', width: 90 },
+      { prop: 'creatorName', label: '创作人', width: 100 },
+      { prop: 'status', label: '状态', width: 80 },
+      { prop: 'createTime', label: '创建时间', width: 170 },
     ],
     formFields: [
-      { prop: 'code', label: '角色编码', type: 'input', required: true },
-      { prop: 'name', label: '显示名', type: 'input', required: true },
-    ],
+      { prop: 'schemeName', label: '方案名称', type: 'input', required: true },
+      { prop: 'mode', label: '模式', type: 'select', options: ['ONE_CLICK', 'PROFESSIONAL', 'CUSTOM', 'EXPERT'], required: true },
+      { prop: 'efficacyType', label: '功效类型', type: 'select', options: ['', '驱寒', '祛湿', '祛风', '化瘀', '活血', '化痰', '养颜', '扶阳'] },
+      { prop: 'creatorType', label: '创作人类型', type: 'select', options: ['admin', 'user', 'expert'], required: true },
+      { prop: 'creatorName', label: '创作人名称', type: 'input' },
+      { prop: 'symptoms', label: '适用症状(逗号分隔)', type: 'input' },
+      { prop: 'targetGender', label: '适用性别', type: 'select', options: ['all', 'male', 'female'] },
+      { prop: 'ageMin', label: '最小年龄', type: 'number' },
+      { prop: 'ageMax', label: '最大年龄', type: 'number' },
+      { prop: 'description', label: '方案描述', type: 'textarea' },
+      { prop: 'status', label: '状态', type: 'select', options: ['draft', 'published', 'archived'] },
+    ],
+    searchFields: [
+      { prop: 'schemeName', label: '方案名称', type: 'input' },
+      { prop: 'mode', label: '模式', type: 'select', options: ['', 'ONE_CLICK', 'PROFESSIONAL', 'CUSTOM', 'EXPERT'] },
+      { prop: 'efficacyType', label: '功效类型', type: 'select', options: ['', '驱寒', '祛湿', '祛风', '化瘀', '活血', '化痰', '养颜', '扶阳'] },
+      { prop: 'status', label: '状态', type: 'select', options: ['', 'draft', 'published', 'archived'] },
+      { prop: 'creatorName', label: '创作人', type: 'input' },
+    ],
+    extraActions: ['viewDetail', 'publish', 'archive'],
   },
 }
 

+ 8 - 20
openspec/aijiuyiBackend/admin-ui/src/layouts/AdminLayout.vue

@@ -13,20 +13,18 @@
       >
         <el-menu-item index="/home">首页</el-menu-item>
         <el-sub-menu index="dict">
-          <template #title>基础数据</template>
+          <template #title>算法参数</template>
           <el-menu-item v-for="m in menuDict" :key="m.path" :index="m.path">{{ m.title }}</el-menu-item>
         </el-sub-menu>
-        <el-sub-menu index="biz">
-          <template #title>业务配置</template>
-          <el-menu-item v-for="m in menuBiz" :key="m.path" :index="m.path">{{ m.title }}</el-menu-item>
-        </el-sub-menu>
         <el-sub-menu index="user">
           <template #title>用户与设备</template>
           <el-menu-item v-for="m in menuUser" :key="m.path" :index="m.path">{{ m.title }}</el-menu-item>
+          <el-menu-item v-for="m in menuBiz" :key="m.path" :index="m.path">{{ m.title }}</el-menu-item>
         </el-sub-menu>
         <el-menu-item index="/admin-users">后台账号</el-menu-item>
         <el-menu-item index="/operation-logs">操作日志</el-menu-item>
         <el-menu-item index="/localization">穴位定位预览</el-menu-item>
+        <el-menu-item index="/simulation">方案模拟测试</el-menu-item>
       </el-menu>
     </el-aside>
     <el-container class="content-wrapper">
@@ -67,27 +65,17 @@ const pageTitle = computed(() => {
 })
 
 const menuDict = [
-  { path: '/crud/symptoms', title: '症状字典' },
-  { path: '/crud/body-regions', title: '身体部位' },
-  { path: '/crud/acupoints', title: '标准穴位' },
+  { path: '/crud/acupoints', title: '穴位管理' },
+  { path: '/crud/moxa-techniques', title: '艾灸手法' },
+  { path: '/crud/schemes', title: '方案管理' },
 ]
 
 const menuBiz = [
-  { path: '/crud/efficacies', title: '功效' },
-  { path: '/crud/techniques', title: '手法' },
-  { path: '/crud/technique-combo-rules', title: '手法组合' },
-  { path: '/crud/device-models', title: '设备型号' },
-  { path: '/crud/algorithm-params', title: '算法参数集' },
-  { path: '/crud/efficacy-versions', title: '功效版本' },
-  { path: '/crud/algorithm-versions', title: '算法版本' },
+  { path: '/crud/devices', title: '设备管理' },
 ]
 
 const menuUser = [
-  { path: '/crud/accounts', title: 'C端账号' },
-  { path: '/crud/end-users', title: 'C端用户' },
-  { path: '/crud/user-fingerprints', title: '指纹' },
-  { path: '/crud/moxa-records', title: '艾灸记录' },
-  { path: '/crud/admin-roles', title: '后台角色' },
+  { path: '/crud/users', title: '用户管理' },
 ]
 
 function logout() {

+ 24 - 0
openspec/aijiuyiBackend/admin-ui/src/router/index.js

@@ -38,6 +38,30 @@ const routes = [
         component: () => import('../views/LocalizationPreviewView.vue'),
         meta: { title: '穴位定位预览' },
       },
+      {
+        path: 'schemes/:schemeId/detail',
+        name: 'scheme-detail',
+        component: () => import('../views/SchemeDetailView.vue'),
+        meta: { title: '方案详情' },
+      },
+      {
+        path: 'users/:userId/devices',
+        name: 'user-devices',
+        component: () => import('../views/UserDevicesView.vue'),
+        meta: { title: '用户设备管理' },
+      },
+      {
+        path: 'devices/:deviceId/users',
+        name: 'device-users',
+        component: () => import('../views/DeviceUsersView.vue'),
+        meta: { title: '设备用户管理' },
+      },
+      {
+        path: 'simulation',
+        name: 'simulation',
+        component: () => import('../views/SchemeSimulationView.vue'),
+        meta: { title: '方案模拟测试' },
+      },
     ],
   },
 ]

+ 22 - 18
openspec/aijiuyiBackend/admin-ui/src/views/AdminUsersView.vue

@@ -4,9 +4,10 @@
       <el-button v-if="canManage" type="primary" @click="openCreate" class="btn-create">新建管理员</el-button>
     </div>
     <el-table :data="rows" class="modern-table" v-loading="loading" style="width: 100%">
-      <el-table-column prop="id" label="ID" width="72" />
+      <el-table-column prop="id" label="ID" width="80" />
       <el-table-column prop="username" label="用户名" width="140" />
       <el-table-column prop="displayName" label="显示名" min-width="120" />
+      <el-table-column prop="role" label="角色" width="120" />
       <el-table-column prop="status" label="状态" width="100" />
       <el-table-column prop="createTime" label="创建时间" width="170" />
       <el-table-column label="操作" width="220" fixed="right">
@@ -38,6 +39,12 @@
         <el-form-item label="显示名" prop="displayName">
           <el-input v-model="createForm.displayName" />
         </el-form-item>
+        <el-form-item label="角色" prop="role">
+          <el-select v-model="createForm.role" style="width: 100%">
+            <el-option label="管理员" value="ADMIN" />
+            <el-option label="超级管理员" value="SUPER_ADMIN" />
+          </el-select>
+        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="createVisible = false">取消</el-button>
@@ -82,15 +89,9 @@ import { computed, reactive, ref } from 'vue'
 import { ElMessageBox } from 'element-plus'
 import http from '../api/http'
 
-const roles = computed(() => {
-  try {
-    return JSON.parse(localStorage.getItem('admin_roles') || '[]')
-  } catch {
-    return []
-  }
-})
-const isSuper = computed(() => roles.value.includes('SUPER_ADMIN'))
-const canManage = computed(() => roles.value.some((r) => r === 'SUPER_ADMIN' || r === 'ADMIN'))
+const role = computed(() => localStorage.getItem('admin_roles') || '')
+const isSuper = computed(() => role.value === 'SUPER_ADMIN')
+const canManage = computed(() => role.value === 'SUPER_ADMIN' || role.value === 'ADMIN')
 
 const rows = ref([])
 const total = ref(0)
@@ -99,10 +100,12 @@ const page = reactive({ current: 1, size: 10 })
 
 const createVisible = ref(false)
 const createRef = ref()
-const createForm = reactive({ username: '', password: '', displayName: '' })
+const createForm = reactive({ username: '', password: '', displayName: '', role: 'ADMIN' })
 const createRules = {
   username: [{ required: true, message: '必填', trigger: 'blur' }],
-  password: [{ required: true, message: '必填', trigger: 'blur' }],
+  password: [{ required: true, message: '必填', trigger: 'blur' }, { min: 8, message: '至少8位', trigger: 'blur' }],
+  displayName: [{ required: true, message: '必填', trigger: 'blur' }],
+  role: [{ required: true, message: '必填', trigger: 'blur' }],
 }
 
 const editVisible = ref(false)
@@ -121,7 +124,7 @@ const saving = ref(false)
 async function fetchList() {
   loading.value = true
   try {
-    const res = await http.get('/v1/admin/users', {
+    const res = await http.get('/admin-users', {
       params: { current: page.current, size: page.size },
     })
     const pr = res.data || {}
@@ -138,6 +141,7 @@ function openCreate() {
   createForm.username = ''
   createForm.password = ''
   createForm.displayName = ''
+  createForm.role = 'ADMIN'
   createVisible.value = true
 }
 
@@ -145,7 +149,7 @@ async function submitCreate() {
   await createRef.value?.validate()
   saving.value = true
   try {
-    await http.post('/v1/admin/users', { ...createForm })
+    await http.post('/admin-users/create', { ...createForm })
     createVisible.value = false
     await fetchList()
   } finally {
@@ -154,7 +158,7 @@ async function submitCreate() {
 }
 
 async function openEdit(row) {
-  const res = await http.get(`/v1/admin/users/${row.id}`)
+  const res = await http.get(`/admin-users/${row.id}`)
   const u = res.data
   editForm.id = u.id
   editForm.username = u.username
@@ -166,7 +170,7 @@ async function openEdit(row) {
 async function submitEdit() {
   saving.value = true
   try {
-    await http.put(`/v1/admin/users/${editForm.id}`, {
+    await http.put(`/admin-users/${editForm.id}`, {
       id: editForm.id,
       username: editForm.username,
       displayName: editForm.displayName,
@@ -189,7 +193,7 @@ async function submitPwd() {
   await pwdRef.value?.validate()
   saving.value = true
   try {
-    await http.put(`/v1/admin/users/${pwdForm.userId}/password`, {
+    await http.put(`/admin-users/${pwdForm.userId}/password`, {
       newPassword: pwdForm.newPassword,
     })
     pwdVisible.value = false
@@ -200,7 +204,7 @@ async function submitPwd() {
 
 async function remove(row) {
   await ElMessageBox.confirm(`删除管理员 ${row.username}?`, '确认', { type: 'warning' })
-  await http.delete(`/v1/admin/users/${row.id}`)
+  await http.delete(`/admin-users/${row.id}`)
   await fetchList()
 }
 </script>

+ 125 - 0
openspec/aijiuyiBackend/admin-ui/src/views/DeviceUsersView.vue

@@ -0,0 +1,125 @@
+<template>
+  <div>
+    <el-page-header @back="goBack" style="margin-bottom: 20px">
+      <template #content><span class="page-title">设备用户管理 - {{ deviceName }}</span></template>
+    </el-page-header>
+
+    <el-card shadow="never" class="section-card">
+      <div class="toolbar">
+        <el-button type="primary" @click="showBindDialog = true">添加用户</el-button>
+      </div>
+
+      <el-table :data="users" class="modern-table" v-loading="loading" style="width: 100%">
+        <el-table-column label="用户ID" width="100">
+          <template #default="{ row }">{{ row.user?.id }}</template>
+        </el-table-column>
+        <el-table-column label="用户姓名" min-width="100">
+          <template #default="{ row }">{{ row.user?.name }}</template>
+        </el-table-column>
+        <el-table-column label="手机号" width="130">
+          <template #default="{ row }">{{ row.user?.phone }}</template>
+        </el-table-column>
+        <el-table-column label="绑定时间" width="170">
+          <template #default="{ row }">{{ row.binding?.boundAt }}</template>
+        </el-table-column>
+        <el-table-column label="是否主用户" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.binding?.isPrimary === 1 ? 'success' : 'info'" size="small">
+              {{ row.binding?.isPrimary === 1 ? '是' : '否' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="setPrimary(row)">设为主用户</el-button>
+            <el-button link type="danger" @click="unbind(row)">解绑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog v-model="showBindDialog" title="添加用户" width="500px">
+      <el-form label-width="80px">
+        <el-form-item label="选择用户">
+          <el-select v-model="bindUserId" filterable placeholder="搜索用户" style="width: 100%">
+            <el-option v-for="u in allUsers" :key="u.id" :label="`${u.name} - ${u.phone}`" :value="u.id" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showBindDialog = false">取消</el-button>
+        <el-button type="primary" @click="bindUser">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import http from '../api/http'
+
+const route = useRoute()
+const router = useRouter()
+const deviceId = route.params.deviceId
+const deviceName = ref('')
+const users = ref([])
+const loading = ref(false)
+const showBindDialog = ref(false)
+const bindUserId = ref(null)
+const allUsers = ref([])
+
+async function fetchUsers() {
+  loading.value = true
+  try {
+    const res = await http.get(`/devices/${deviceId}/users`)
+    users.value = res.data || []
+  } finally {
+    loading.value = false
+  }
+}
+
+async function fetchDevice() {
+  const res = await http.get(`/devices/${deviceId}`)
+  deviceName.value = res.data?.deviceName || deviceId
+}
+
+async function fetchAllUsers() {
+  const res = await http.get('/users', { params: { current: 1, size: 1000 } })
+  allUsers.value = res.data?.records || []
+}
+
+async function bindUser() {
+  if (!bindUserId.value) return
+  await http.post(`/devices/${deviceId}/users/${bindUserId.value}/bind`)
+  ElMessage.success('绑定成功')
+  showBindDialog.value = false
+  await fetchUsers()
+}
+
+async function unbind(row) {
+  await ElMessageBox.confirm('确定解绑此用户?', '确认', { type: 'warning' })
+  await http.delete(`/devices/${deviceId}/users/${row.binding.userId}/unbind`)
+  ElMessage.success('解绑成功')
+  await fetchUsers()
+}
+
+async function setPrimary(row) {
+  await http.put(`/devices/${deviceId}/users/${row.binding.userId}/primary`)
+  ElMessage.success('已设为主用户')
+  await fetchUsers()
+}
+
+function goBack() { router.push('/crud/devices') }
+
+onMounted(() => { fetchDevice(); fetchUsers(); fetchAllUsers() })
+</script>
+
+<style scoped>
+.page-title { font-size: 18px; font-weight: 600; color: #334155; }
+.section-card { border: none; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,.02); }
+.toolbar { display: flex; gap: 12px; margin-bottom: 16px; }
+.modern-table { border-radius: 8px; overflow: hidden; }
+.modern-table :deep(th.el-table__cell) { background-color: #f8fafc; color: #475569; font-weight: 600; }
+</style>

+ 14 - 41
openspec/aijiuyiBackend/admin-ui/src/views/LocalizationPreviewView.vue

@@ -41,7 +41,7 @@
           <el-option
             v-for="u in userOptions"
             :key="u.id"
-            :label="`${u.displayName || '未命名'} (ID: ${u.id})`"
+            :label="`${u.name || '未命名'} (ID: ${u.id})`"
             :value="u.id"
           />
         </el-select>
@@ -64,8 +64,8 @@
       </el-form-item>
       <el-form-item label="性别">
         <el-select v-model="form.user.gender" placeholder="可选" clearable style="width: 100%">
-          <el-option label="男" value="M" />
-          <el-option label="女" value="F" />
+          <el-option label="男" value="male" />
+          <el-option label="女" value="female" />
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -380,8 +380,8 @@ function fmt(v) {
 async function loadUserOptions() {
   usersLoading.value = true
   try {
-    const res = await http.get('/v1/end-users/options')
-    userOptions.value = Array.isArray(res.data) ? res.data : []
+    const res = await http.get('/users', { params: { current: 1, size: 500 } })
+    userOptions.value = Array.isArray(res.data?.records) ? res.data.records : []
   } catch {
     userOptions.value = []
   } finally {
@@ -401,11 +401,11 @@ function onUserSelect(userId) {
   }
   const u = userOptions.value.find((x) => x.id === userId)
   if (!u) return
-  form.user.heightCm = u.heightCm ?? ''
-  form.user.weightKg = u.weightKg ?? ''
-  form.user.shoulderWidthCm = u.shoulderWidthCm ?? ''
-  form.user.spineLengthCm = u.spineLengthCm ?? ''
-  form.user.fingerCunCm = u.fingerCunCm ?? ''
+  form.user.heightCm = ''
+  form.user.weightKg = ''
+  form.user.shoulderWidthCm = u.shoulderWidth ?? ''
+  form.user.spineLengthCm = u.bodyLength ?? ''
+  form.user.fingerCunCm = u.fingerCun1 ?? ''
   form.user.gender = u.gender ?? ''
 }
 
@@ -413,7 +413,7 @@ function onUserSelect(userId) {
 async function loadAcupointOptions() {
   optionsLoading.value = true
   try {
-    const res = await http.get('/v1/acupoints', { params: { current: 1, size: 5000 } })
+    const res = await http.get('/acupoints', { params: { current: 1, size: 5000 } })
     const records = res.data?.records ?? []
     acupointOptions.value = records
       .filter((a) => a.code)
@@ -433,41 +433,14 @@ onMounted(() => {
 
 // ── 调用预览 ──
 async function preview() {
-  const acupointCodes = [...selectedCodes.value].filter(Boolean)
-  if (!acupointCodes.length) {
-    result.value = { error: '请至少选择一个穴位' }
-    return
-  }
-
-  if (!form.user.shoulderWidthCm) {
-    result.value = { error: '肩宽为必填项' }
-    return
-  }
-  if (!form.user.spineLengthCm) {
-    result.value = { error: '脊柱长度为必填项' }
-    return
-  }
-  if (!form.user.fingerCunCm && (!form.user.heightCm || !form.user.weightKg)) {
-    result.value = { error: '未填写指寸值时,身高和体重为必填项' }
+  if (!form.user.userId) {
+    result.value = { error: '请先选择一个用户' }
     return
   }
 
   loading.value = true
   try {
-    const user = {}
-    if (form.user.userId != null && form.user.userId !== '') user.userId = form.user.userId
-    if (form.user.heightCm !== '') user.heightCm = form.user.heightCm
-    if (form.user.weightKg !== '') user.weightKg = form.user.weightKg
-    if (form.user.shoulderWidthCm !== '') user.shoulderWidthCm = form.user.shoulderWidthCm
-    if (form.user.spineLengthCm !== '') user.spineLengthCm = form.user.spineLengthCm
-    if (form.user.fingerCunCm !== '') user.fingerCunCm = form.user.fingerCunCm
-    if (form.user.gender !== '') user.gender = form.user.gender
-    const body = {
-      acupointCodes,
-      deviceModelCode: form.deviceModelCode || undefined,
-      user: Object.keys(user).length ? user : undefined,
-    }
-    const res = await http.post('/preview', body)
+    const res = await http.post(`/users/${form.user.userId}/acupoint-coordinates`)
     result.value = res.data
   } catch (e) {
     result.value = { error: e.message }

+ 2 - 2
openspec/aijiuyiBackend/admin-ui/src/views/Login.vue

@@ -45,14 +45,14 @@ async function submit() {
   await formRef.value?.validate()
   loading.value = true
   try {
-    const res = await http.post('/v1/auth/login', {
+    const res = await http.post('/auth/login', {
       username: form.username,
       password: form.password,
     })
     const data = res.data
     localStorage.setItem('admin_token', data.token)
     localStorage.setItem('admin_username', data.username)
-    localStorage.setItem('admin_roles', JSON.stringify(data.roles || []))
+    localStorage.setItem('admin_roles', data.role || '')
     const redirect = route.query.redirect || '/home'
     router.replace(typeof redirect === 'string' ? redirect : '/home')
   } finally {

+ 6 - 6
openspec/aijiuyiBackend/admin-ui/src/views/OperationLogsView.vue

@@ -1,11 +1,11 @@
 <template>
   <el-card shadow="never" class="modern-card">
     <el-table :data="rows" class="modern-table" v-loading="loading" style="width: 100%">
-      <el-table-column prop="id" label="ID" width="72" />
-      <el-table-column prop="username" label="操作人" width="120" />
-      <el-table-column prop="action" label="动作" min-width="200" show-overflow-tooltip />
-      <el-table-column prop="resourceType" label="资源" width="120" />
-      <el-table-column prop="resourceId" label="资源ID" width="100" />
+      <el-table-column prop="id" label="ID" width="80" />
+      <el-table-column prop="operatorName" label="操作人" width="120" />
+      <el-table-column prop="actionType" label="动作类型" width="100" />
+      <el-table-column prop="module" label="模块" width="120" />
+      <el-table-column prop="content" label="操作内容" min-width="200" show-overflow-tooltip />
       <el-table-column prop="ip" label="IP" width="130" />
       <el-table-column prop="createTime" label="时间" width="170" />
     </el-table>
@@ -33,7 +33,7 @@ const page = reactive({ current: 1, size: 20 })
 async function fetchList() {
   loading.value = true
   try {
-    const res = await http.get('/v1/operation-logs', {
+    const res = await http.get('/operation-logs', {
       params: { current: page.current, size: page.size },
     })
     const pr = res.data || {}

+ 136 - 0
openspec/aijiuyiBackend/admin-ui/src/views/SchemeDetailView.vue

@@ -0,0 +1,136 @@
+<template>
+  <div v-loading="loading">
+    <el-page-header @back="goBack" style="margin-bottom: 20px">
+      <template #content><span class="page-title">方案详情</span></template>
+    </el-page-header>
+
+    <template v-if="detail">
+      <!-- 基本信息 -->
+      <el-card shadow="never" class="section-card">
+        <template #header><span class="section-title">基本信息</span></template>
+        <el-descriptions :column="3" border>
+          <el-descriptions-item label="方案ID">{{ detail.scheme.id }}</el-descriptions-item>
+          <el-descriptions-item label="方案名称">{{ detail.scheme.schemeName }}</el-descriptions-item>
+          <el-descriptions-item label="模式类型">{{ detail.scheme.mode }}</el-descriptions-item>
+          <el-descriptions-item label="功效类型">{{ detail.scheme.efficacyType || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="创作人">{{ detail.scheme.creatorName }}</el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="detail.scheme.status === 'published' ? 'success' : detail.scheme.status === 'archived' ? 'info' : 'warning'">
+              {{ detail.scheme.status }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="适用性别">{{ detail.scheme.targetGender }}</el-descriptions-item>
+          <el-descriptions-item label="适用年龄">{{ detail.scheme.ageMin }}-{{ detail.scheme.ageMax }}岁</el-descriptions-item>
+          <el-descriptions-item label="创建时间">{{ detail.scheme.createTime }}</el-descriptions-item>
+          <el-descriptions-item label="方案描述" :span="3">{{ detail.scheme.description || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-card>
+
+      <!-- 治疗步骤 -->
+      <el-card shadow="never" class="section-card">
+        <template #header>
+          <div class="section-header">
+            <span class="section-title">治疗步骤</span>
+            <span class="section-info">总时长:{{ Math.round(detail.totalDurationSeconds / 60) }} 分钟</span>
+          </div>
+        </template>
+        <el-table :data="detail.steps" class="modern-table" style="width: 100%">
+          <el-table-column prop="stepOrder" label="步骤" width="70" />
+          <el-table-column prop="acupointName" label="穴位名称" width="120" />
+          <el-table-column prop="side" label="侧别" width="80" />
+          <el-table-column prop="temperature" label="温度(°C)" width="100" />
+          <el-table-column label="时间" width="100">
+            <template #default="{ row }">{{ Math.round(row.duration / 60) }}分钟</template>
+          </el-table-column>
+          <el-table-column prop="techniqueName" label="艾灸手法" width="120" />
+          <el-table-column prop="remark" label="备注" min-width="120" />
+        </el-table>
+      </el-card>
+
+      <!-- 使用统计 -->
+      <el-card shadow="never" class="section-card">
+        <template #header><span class="section-title">使用统计</span></template>
+        <el-descriptions :column="4" border>
+          <el-descriptions-item label="使用次数">{{ detail.statistics?.totalSessions || 0 }}</el-descriptions-item>
+          <el-descriptions-item label="使用人数">{{ detail.statistics?.userCount || 0 }}</el-descriptions-item>
+          <el-descriptions-item label="平均评分">{{ detail.statistics?.averageRating || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="最后使用">{{ detail.statistics?.lastUsedAt || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-card>
+
+      <!-- 最近使用记录 -->
+      <el-card v-if="detail.recentRecords?.length" shadow="never" class="section-card">
+        <template #header><span class="section-title">最近使用记录(最近10条)</span></template>
+        <el-table :data="detail.recentRecords" class="modern-table" style="width: 100%">
+          <el-table-column prop="userId" label="用户ID" width="100" />
+          <el-table-column prop="deviceId" label="设备ID" width="100" />
+          <el-table-column prop="startedAt" label="开始时间" width="170" />
+          <el-table-column label="时长" width="100">
+            <template #default="{ row }">{{ row.duration ? Math.round(row.duration / 60) + '分钟' : '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="rating" label="评分" width="80" />
+          <el-table-column prop="remark" label="备注" min-width="120" />
+        </el-table>
+      </el-card>
+
+      <!-- 操作按钮 -->
+      <div class="action-bar">
+        <el-button v-if="detail.scheme.status === 'draft'" type="primary" @click="publishScheme">发布方案</el-button>
+        <el-button v-if="detail.scheme.status === 'published'" type="warning" @click="archiveScheme">下架方案</el-button>
+        <el-button @click="goBack">返回</el-button>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import http from '../api/http'
+
+const route = useRoute()
+const router = useRouter()
+const loading = ref(false)
+const detail = ref(null)
+const schemeId = route.params.schemeId
+
+async function fetchDetail() {
+  loading.value = true
+  try {
+    const res = await http.get(`/schemes/${schemeId}/detail`)
+    detail.value = res.data
+  } finally {
+    loading.value = false
+  }
+}
+
+async function publishScheme() {
+  await http.post(`/schemes/${schemeId}/publish`)
+  ElMessage.success('发布成功')
+  await fetchDetail()
+}
+
+async function archiveScheme() {
+  await http.post(`/schemes/${schemeId}/archive`)
+  ElMessage.success('下架成功')
+  await fetchDetail()
+}
+
+function goBack() {
+  router.push('/crud/schemes')
+}
+
+onMounted(fetchDetail)
+</script>
+
+<style scoped>
+.page-title { font-size: 18px; font-weight: 600; color: #334155; }
+.section-card { border: none; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,.02); margin-bottom: 16px; }
+.section-title { font-weight: 600; color: #334155; }
+.section-header { display: flex; justify-content: space-between; align-items: center; }
+.section-info { color: #64748b; font-size: 14px; }
+.action-bar { display: flex; gap: 12px; margin-top: 16px; justify-content: flex-end; }
+.modern-table { border-radius: 8px; overflow: hidden; }
+.modern-table :deep(th.el-table__cell) { background-color: #f8fafc; color: #475569; font-weight: 600; }
+</style>

+ 753 - 0
openspec/aijiuyiBackend/admin-ui/src/views/SchemeSimulationView.vue

@@ -0,0 +1,753 @@
+<template>
+  <div class="simulation-page">
+    <el-page-header @back="() => $router.push('/home')" style="margin-bottom: 20px">
+      <template #content><span class="page-title">方案模拟测试</span></template>
+      <template #extra>
+        <el-tag type="info" effect="plain">模拟小程序选择手法/模式后,系统对穴位的判断结果</el-tag>
+      </template>
+    </el-page-header>
+
+    <div class="sim-layout">
+      <!-- 左侧:配置面板 -->
+      <div class="sim-config">
+        <el-card shadow="never" class="config-card">
+          <template #header><span class="card-title">模拟配置</span></template>
+
+          <!-- 用户选择 -->
+          <el-form label-width="90px" class="config-form">
+            <el-form-item label="选择用户">
+              <el-select
+                v-model="selectedUserId"
+                filterable
+                clearable
+                placeholder="选择已有用户(自动填充体型数据)"
+                style="width: 100%"
+                :loading="usersLoading"
+                @change="onUserSelect"
+              >
+                <el-option
+                  v-for="u in userOptions"
+                  :key="u.id"
+                  :label="`${u.name || '未命名'} (${u.gender === 'male' ? '男' : '女'}, ${u.shoulderWidth || '-'}cm)`"
+                  :value="u.id"
+                />
+              </el-select>
+            </el-form-item>
+
+            <el-divider content-position="left">体型参数</el-divider>
+
+            <el-row :gutter="12">
+              <el-col :span="12">
+                <el-form-item label="性别">
+                  <el-select v-model="bodyForm.gender" style="width: 100%" disabled>
+                    <el-option label="男" value="male" />
+                    <el-option label="女" value="female" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="年龄">
+                  <el-input v-model="bodyForm.age" disabled />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="12">
+              <el-col :span="12">
+                <el-form-item label="肩宽(cm)">
+                  <el-input v-model="bodyForm.shoulderWidth" disabled />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="身长(cm)">
+                  <el-input v-model="bodyForm.bodyLength" disabled />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="12">
+              <el-col :span="8">
+                <el-form-item label="一寸">
+                  <el-input v-model="bodyForm.fingerCun1" disabled placeholder="-" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="1.5寸">
+                  <el-input v-model="bodyForm.fingerCun1_5" disabled placeholder="-" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="三寸">
+                  <el-input v-model="bodyForm.fingerCun3" disabled placeholder="-" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <div v-if="bodyForm.acupointTableStatus" class="status-badge">
+              <el-tag :type="bodyForm.acupointTableStatus === 'GENERATED' ? 'success' : 'warning'" size="small">
+                穴位表: {{ bodyForm.acupointTableStatus === 'GENERATED' ? '已生成' : '未生成' }}
+              </el-tag>
+            </div>
+
+            <el-divider content-position="left">选择模式与方案</el-divider>
+
+            <el-form-item label="艾灸模式">
+              <el-select v-model="selectedMode" placeholder="选择模式" style="width: 100%" @change="onModeChange">
+                <el-option label="一键艾灸(默认)" value="ONE_CLICK" />
+                <el-option label="专业模式(8种功效)" value="PROFESSIONAL" />
+                <el-option label="自定义模式" value="CUSTOM" />
+                <el-option label="延年圣手模式(专家方案)" value="EXPERT" />
+              </el-select>
+            </el-form-item>
+
+            <el-form-item v-if="selectedMode === 'PROFESSIONAL'" label="功效类型">
+              <el-select v-model="selectedEfficacy" placeholder="选择功效" style="width: 100%" @change="onEfficacyChange">
+                <el-option v-for="e in efficacyTypes" :key="e" :label="e" :value="e" />
+              </el-select>
+            </el-form-item>
+
+            <el-form-item label="选择方案">
+              <el-select
+                v-model="selectedSchemeId"
+                filterable
+                placeholder="选择具体方案"
+                style="width: 100%"
+                :loading="schemesLoading"
+                :disabled="!filteredSchemes.length"
+              >
+                <el-option
+                  v-for="s in filteredSchemes"
+                  :key="s.id"
+                  :label="`${s.schemeName} (${s.efficacyType || s.mode})`"
+                  :value="s.id"
+                />
+              </el-select>
+            </el-form-item>
+
+            <el-form-item>
+              <el-button
+                type="primary"
+                size="large"
+                :loading="simulating"
+                :disabled="!canSimulate"
+                @click="runSimulation"
+                style="width: 100%"
+              >
+                开始模拟测试
+              </el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+
+        <!-- 方案步骤结果 -->
+        <el-card v-if="pushData" shadow="never" class="result-card steps-card">
+          <template #header>
+            <div class="card-header-row">
+              <span class="card-title">模拟结果 — 治疗步骤</span>
+              <el-tag effect="plain" size="small">
+                总时长: {{ totalMinutes }} 分钟 / {{ pushData.scheme?.steps?.length || 0 }} 步
+              </el-tag>
+            </div>
+          </template>
+          <el-table :data="pushData.scheme?.steps || []" class="modern-table" size="small" stripe>
+            <el-table-column prop="stepOrder" label="#" width="45" align="center" />
+            <el-table-column prop="acupointName" label="穴位" width="100" />
+            <el-table-column label="偏移X(mm)" width="90" align="right">
+              <template #default="{ row }">
+                <span class="num-cell">{{ fmtNum(row.offsetX) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="偏移Y(mm)" width="90" align="right">
+              <template #default="{ row }">
+                <span class="num-cell">{{ fmtNum(row.offsetY) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="temperature" label="温度°C" width="75" align="center" />
+            <el-table-column label="时长" width="70" align="center">
+              <template #default="{ row }">{{ Math.round(row.duration / 60) }}分</template>
+            </el-table-column>
+            <el-table-column prop="techniqueName" label="手法" min-width="80" />
+          </el-table>
+        </el-card>
+      </div>
+
+      <!-- 右侧:可视化 -->
+      <div class="sim-visual">
+        <el-card shadow="never" class="visual-card">
+          <template #header>
+            <div class="card-header-row">
+              <span class="card-title">穴位可视化</span>
+              <span v-if="pushData" class="visual-sub">
+                {{ pushData.userName }} · {{ modeLabel(pushData.mode) }} · {{ pushData.scheme?.schemeName }}
+              </span>
+            </div>
+          </template>
+
+          <div class="body-canvas-wrap">
+            <svg viewBox="0 0 400 580" xmlns="http://www.w3.org/2000/svg" class="body-svg">
+              <defs>
+                <pattern id="sim-grid" width="20" height="20" patternUnits="userSpaceOnUse">
+                  <path d="M 20 0 L 0 0 0 20" fill="none" stroke="rgba(99,102,241,0.05)" stroke-width="1" />
+                </pattern>
+                <radialGradient id="simWash" cx="50%" cy="18%" r="75%">
+                  <stop offset="0%" stop-color="#f8fafc" />
+                  <stop offset="55%" stop-color="#f1f5f9" />
+                  <stop offset="100%" stop-color="#e8eef5" />
+                </radialGradient>
+                <linearGradient id="simBodyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
+                  <stop offset="0%" stop-color="#fdf8f5" />
+                  <stop offset="35%" stop-color="#f5ebe3" />
+                  <stop offset="100%" stop-color="#e8ddd4" />
+                </linearGradient>
+                <linearGradient id="simStroke" x1="0%" y1="0%" x2="0%" y2="100%">
+                  <stop offset="0%" stop-color="#c9b8a8" />
+                  <stop offset="100%" stop-color="#a8988a" />
+                </linearGradient>
+                <filter id="simShadow" x="-25%" y="-15%" width="150%" height="130%">
+                  <feDropShadow dx="0" dy="6" stdDeviation="8" flood-color="#5c4a3d" flood-opacity="0.10" />
+                </filter>
+              </defs>
+
+              <rect width="100%" height="100%" fill="url(#simWash)" rx="14" />
+              <rect width="100%" height="100%" fill="url(#sim-grid)" rx="14" opacity="0.85" />
+
+              <line class="axis-line" x1="200" y1="12" x2="200" y2="568" stroke="rgba(148,163,184,0.4)" stroke-width="1" stroke-dasharray="4 6" />
+              <line class="axis-line" x1="12" y1="100" x2="388" y2="100" stroke="rgba(148,163,184,0.4)" stroke-width="1" stroke-dasharray="5 5" />
+
+              <!-- 人体轮廓 -->
+              <g filter="url(#simShadow)" stroke="url(#simStroke)" stroke-width="1.35" stroke-linejoin="round" stroke-linecap="round">
+                <path d="
+                  M 200 28
+                  C 186 28, 174 38, 172 56
+                  C 170 70, 174 86, 178 96
+                  C 180 102, 162 108, 138 114
+                  C 114 124, 98 136, 88 150
+                  C 80 168, 76 192, 74 220
+                  C 72 252, 68 288, 64 318
+                  C 60 340, 54 352, 62 358
+                  C 68 364, 84 356, 96 332
+                  C 108 306, 112 272, 116 238
+                  C 120 208, 124 186, 128 192
+                  C 134 204, 136 238, 138 278
+                  C 142 330, 146 374, 148 408
+                  C 152 454, 144 510, 138 548
+                  C 134 568, 130 578, 144 582
+                  C 160 586, 170 582, 176 556
+                  C 182 514, 190 452, 200 424
+                  C 210 452, 218 514, 224 556
+                  C 230 582, 240 586, 256 582
+                  C 270 578, 266 568, 262 548
+                  C 256 510, 248 454, 252 408
+                  C 254 374, 258 330, 262 278
+                  C 264 238, 266 204, 272 192
+                  C 276 186, 280 208, 284 238
+                  C 288 272, 292 306, 304 332
+                  C 316 356, 332 364, 338 358
+                  C 346 352, 340 340, 336 318
+                  C 332 288, 328 252, 326 220
+                  C 324 192, 320 168, 312 150
+                  C 304 136, 286 124, 262 114
+                  C 238 108, 220 102, 222 96
+                  C 226 86, 230 70, 228 56
+                  C 226 38, 214 28, 200 28 Z" fill="url(#simBodyGrad)" />
+                <path d="M 148 126 C 136 134, 124 158, 132 192 C 136 208, 152 212, 158 196 C 164 174, 162 142, 148 126 Z" fill="none" stroke="rgba(92,74,61,0.18)" stroke-width="1.1" />
+                <path d="M 252 126 C 264 134, 276 158, 268 192 C 264 208, 248 212, 242 196 C 236 174, 238 142, 252 126 Z" fill="none" stroke="rgba(92,74,61,0.18)" stroke-width="1.1" />
+                <path d="M 148 372 C 170 402, 186 412, 200 406 C 214 412, 230 402, 252 372" fill="none" stroke="rgba(92,74,61,0.14)" stroke-width="1.1" />
+              </g>
+
+              <line x1="200" y1="92" x2="200" y2="390" stroke="rgba(99,102,241,0.35)" stroke-width="2" stroke-dasharray="8 6" stroke-linecap="round" />
+
+              <!-- C7 大椎穴标记 -->
+              <g transform="translate(200, 100)">
+                <circle r="14" fill="none" stroke="#e85d5d" stroke-width="1" opacity="0.3">
+                  <animate attributeName="r" values="8;16;8" dur="2.5s" repeatCount="indefinite" />
+                  <animate attributeName="opacity" values="0.5;0;0.5" dur="2.5s" repeatCount="indefinite" />
+                </circle>
+                <circle r="5" fill="#fff" stroke="#e85d5d" stroke-width="2" />
+                <line x1="-11" y1="0" x2="-5" y2="0" stroke="#e85d5d" stroke-width="1.8" stroke-linecap="round" />
+                <line x1="5" y1="0" x2="11" y2="0" stroke="#e85d5d" stroke-width="1.8" stroke-linecap="round" />
+                <line x1="0" y1="-11" x2="0" y2="-5" stroke="#e85d5d" stroke-width="1.8" stroke-linecap="round" />
+                <line x1="0" y1="5" x2="0" y2="11" stroke="#e85d5d" stroke-width="1.8" stroke-linecap="round" />
+                <text x="16" y="4" font-size="11" fill="#c24141" font-weight="700" font-family="system-ui, sans-serif">C7 大椎穴</text>
+              </g>
+
+              <text x="208" y="24" font-size="10" fill="#94a3b8" font-family="system-ui, sans-serif">↑ 头侧</text>
+              <text x="208" y="570" font-size="10" fill="#94a3b8" font-family="system-ui, sans-serif">↓ 尾侧</text>
+              <text x="14" y="92" font-size="10" fill="#94a3b8" font-family="system-ui, sans-serif">患者左 (−X)</text>
+              <text x="386" y="92" text-anchor="end" font-size="10" fill="#94a3b8" font-family="system-ui, sans-serif">患者右 (+X)</text>
+
+              <!-- 穴位标记 -->
+              <g v-for="(m, idx) in markers" :key="idx">
+                <circle :cx="m.x" :cy="m.y" r="12" :fill="m.color" opacity="0.18" />
+                <circle :cx="m.x" :cy="m.y" r="6" :fill="m.color" stroke="#fff" stroke-width="2" />
+                <text :x="m.x" :y="m.y + 4" text-anchor="middle" font-size="8" font-weight="800" fill="#fff" font-family="system-ui, sans-serif" style="pointer-events:none">
+                  {{ m.num }}
+                </text>
+                <text
+                  :x="m.labelX" :y="m.y + 4"
+                  :text-anchor="m.anchor"
+                  font-size="10" font-weight="700" :fill="m.color"
+                  font-family="system-ui, sans-serif"
+                  paint-order="stroke fill"
+                  stroke="rgba(255,255,255,0.92)" stroke-width="3" stroke-linejoin="round"
+                >
+                  {{ m.name }}
+                </text>
+                <!-- 步骤连接线 -->
+                <line
+                  v-if="idx > 0"
+                  :x1="markers[idx-1].x" :y1="markers[idx-1].y"
+                  :x2="m.x" :y2="m.y"
+                  stroke="rgba(16,185,129,0.25)" stroke-width="1.5" stroke-dasharray="4 4"
+                />
+              </g>
+
+              <!-- 空状态 -->
+              <g v-if="!markers.length">
+                <rect x="80" y="260" width="240" height="52" rx="26" fill="rgba(255,255,255,0.72)" stroke="rgba(148,163,184,0.35)" stroke-width="1" />
+                <text x="200" y="282" text-anchor="middle" font-size="13" fill="#475569" font-weight="600" font-family="system-ui, sans-serif">等待模拟</text>
+                <text x="200" y="300" text-anchor="middle" font-size="11" fill="#94a3b8" font-family="system-ui, sans-serif">选择用户和方案后点击「开始模拟测试」</text>
+              </g>
+            </svg>
+          </div>
+
+          <!-- 穴位图例 -->
+          <div v-if="markers.length" class="marker-legend">
+            <div class="legend-header">
+              <span class="legend-title">穴位定位结果</span>
+              <div class="legend-chips">
+                <span class="chip"><i class="dot dot-step" />步骤顺序</span>
+                <span class="chip"><i class="dot dot-coord" />坐标偏移(mm)</span>
+              </div>
+            </div>
+            <div class="legend-cards">
+              <div v-for="(m, idx) in markers" :key="idx" class="legend-item" :style="{ borderLeftColor: m.color }">
+                <div class="legend-num" :style="{ background: m.color }">{{ m.num }}</div>
+                <div class="legend-info">
+                  <div class="legend-name">{{ m.name }}</div>
+                  <div class="legend-detail">
+                    偏移: ({{ fmtNum(m.rawOffsetX) }}, {{ fmtNum(m.rawOffsetY) }}) mm ·
+                    {{ m.technique }} · {{ m.temp }}°C · {{ m.mins }}分钟
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-card>
+
+        <!-- 推送数据 JSON -->
+        <el-card v-if="pushData" shadow="never" class="json-card">
+          <template #header>
+            <div class="card-header-row">
+              <span class="card-title">推送数据预览(设备接收格式)</span>
+              <el-button size="small" @click="copyJson">复制 JSON</el-button>
+            </div>
+          </template>
+          <pre class="json-block">{{ pushDataJson }}</pre>
+        </el-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import http from '../api/http'
+
+const selectedUserId = ref(null)
+const selectedMode = ref('')
+const selectedEfficacy = ref('')
+const selectedSchemeId = ref(null)
+
+const userOptions = ref([])
+const usersLoading = ref(false)
+const allSchemes = ref([])
+const schemesLoading = ref(false)
+const simulating = ref(false)
+const pushData = ref(null)
+
+const efficacyTypes = ['驱寒', '祛湿', '祛风', '化瘀', '活血', '化痰', '养颜', '扶阳']
+
+const bodyForm = reactive({
+  gender: '',
+  age: '',
+  shoulderWidth: '',
+  bodyLength: '',
+  fingerCun1: '',
+  fingerCun1_5: '',
+  fingerCun3: '',
+  acupointTableStatus: '',
+})
+
+const canSimulate = computed(() => selectedUserId.value && selectedSchemeId.value)
+
+const filteredSchemes = computed(() => {
+  if (!selectedMode.value) return []
+  let list = allSchemes.value.filter((s) => s.mode === selectedMode.value)
+  if (selectedMode.value === 'PROFESSIONAL' && selectedEfficacy.value) {
+    list = list.filter((s) => s.efficacyType === selectedEfficacy.value)
+  }
+  return list
+})
+
+const totalMinutes = computed(() => {
+  const steps = pushData.value?.scheme?.steps || []
+  const totalSec = steps.reduce((sum, s) => sum + (s.duration || 0), 0)
+  return Math.round(totalSec / 60)
+})
+
+const pushDataJson = computed(() => {
+  if (!pushData.value) return ''
+  try { return JSON.stringify(pushData.value, null, 2) }
+  catch { return String(pushData.value) }
+})
+
+// SVG coordinate mapping
+const SVG_C7_X = 200
+const SVG_C7_Y = 100
+const SCALE = 0.38
+
+function bodyToSvg(offsetXmm, offsetYmm) {
+  return {
+    x: SVG_C7_X + Number(offsetXmm) * SCALE,
+    y: SVG_C7_Y - Number(offsetYmm) * SCALE,
+  }
+}
+
+const markers = computed(() => {
+  const steps = pushData.value?.scheme?.steps || []
+  return steps
+    .filter((s) => s.offsetX != null && s.offsetY != null)
+    .map((s, idx) => {
+      const { x, y } = bodyToSvg(s.offsetX, s.offsetY)
+      const techniqueColors = {
+        '温和灸': '#10b981',
+        '雀啄灸': '#f59e0b',
+        '回旋灸': '#6366f1',
+        '麻雀灸': '#ec4899',
+        '一键艾灸': '#3b82f6',
+      }
+      const color = techniqueColors[s.techniqueName] || '#10b981'
+      const onRight = x > SVG_C7_X
+      return {
+        num: s.stepOrder || idx + 1,
+        x, y, color,
+        name: s.acupointName,
+        labelX: onRight ? x - 14 : x + 14,
+        anchor: onRight ? 'end' : 'start',
+        rawOffsetX: s.offsetX,
+        rawOffsetY: s.offsetY,
+        technique: s.techniqueName,
+        temp: s.temperature,
+        mins: Math.round((s.duration || 0) / 60),
+      }
+    })
+})
+
+function modeLabel(mode) {
+  const map = { ONE_CLICK: '一键艾灸', PROFESSIONAL: '专业模式', CUSTOM: '自定义模式', EXPERT: '延年圣手' }
+  return map[mode] || mode
+}
+
+function fmtNum(v) {
+  return v != null ? Number(v).toFixed(1) : '-'
+}
+
+async function loadUsers() {
+  usersLoading.value = true
+  try {
+    const res = await http.get('/users', { params: { current: 1, size: 500 } })
+    userOptions.value = Array.isArray(res.data?.records) ? res.data.records : []
+  } catch { userOptions.value = [] }
+  finally { usersLoading.value = false }
+}
+
+async function loadSchemes() {
+  schemesLoading.value = true
+  try {
+    const res = await http.get('/schemes', { params: { current: 1, size: 500 } })
+    allSchemes.value = Array.isArray(res.data?.records)
+      ? res.data.records.filter((s) => s.status === 'published')
+      : []
+  } catch { allSchemes.value = [] }
+  finally { schemesLoading.value = false }
+}
+
+function onUserSelect(userId) {
+  pushData.value = null
+  if (!userId) {
+    Object.keys(bodyForm).forEach((k) => (bodyForm[k] = ''))
+    return
+  }
+  const u = userOptions.value.find((x) => x.id === userId)
+  if (!u) return
+  bodyForm.gender = u.gender || ''
+  bodyForm.age = u.age || ''
+  bodyForm.shoulderWidth = u.shoulderWidth ?? ''
+  bodyForm.bodyLength = u.bodyLength ?? ''
+  bodyForm.fingerCun1 = u.fingerCun1 ?? ''
+  bodyForm.fingerCun1_5 = u.fingerCun1_5 ?? ''
+  bodyForm.fingerCun3 = u.fingerCun3 ?? ''
+  bodyForm.acupointTableStatus = u.acupointTableStatus || ''
+}
+
+function onModeChange() {
+  selectedSchemeId.value = null
+  selectedEfficacy.value = ''
+  pushData.value = null
+
+  if (selectedMode.value === 'ONE_CLICK') {
+    const oneClick = allSchemes.value.find((s) => s.mode === 'ONE_CLICK')
+    if (oneClick) selectedSchemeId.value = oneClick.id
+  }
+}
+
+function onEfficacyChange() {
+  selectedSchemeId.value = null
+  const match = filteredSchemes.value[0]
+  if (match) selectedSchemeId.value = match.id
+}
+
+async function runSimulation() {
+  if (!selectedUserId.value || !selectedSchemeId.value) {
+    ElMessage.warning('请先选择用户和方案')
+    return
+  }
+
+  simulating.value = true
+  try {
+    const res = await http.get(`/users/${selectedUserId.value}/push-data/${selectedSchemeId.value}`)
+    pushData.value = res.data
+    ElMessage.success('模拟完成,查看右侧可视化结果')
+  } catch (e) {
+    ElMessage.error('模拟失败: ' + (e.message || '未知错误'))
+  } finally {
+    simulating.value = false
+  }
+}
+
+function copyJson() {
+  navigator.clipboard.writeText(pushDataJson.value).then(
+    () => ElMessage.success('已复制'),
+    () => ElMessage.error('复制失败')
+  )
+}
+
+onMounted(() => {
+  loadUsers()
+  loadSchemes()
+})
+</script>
+
+<style scoped>
+.simulation-page {
+  max-width: 1600px;
+  margin: 0 auto;
+}
+.page-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #334155;
+}
+
+.sim-layout {
+  display: flex;
+  gap: 24px;
+  align-items: flex-start;
+}
+.sim-config {
+  flex: 0 0 480px;
+  min-width: 380px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+.sim-visual {
+  flex: 1;
+  min-width: 420px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.config-card,
+.result-card,
+.visual-card,
+.json-card {
+  border: none;
+  border-radius: 14px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
+}
+.card-title {
+  font-weight: 600;
+  color: #334155;
+  font-size: 15px;
+}
+.card-header-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.config-form :deep(.el-form-item) {
+  margin-bottom: 14px;
+}
+.config-form :deep(.el-form-item__label) {
+  font-size: 13px;
+  color: #64748b;
+}
+
+.status-badge {
+  text-align: right;
+  margin-bottom: 4px;
+}
+
+.steps-card :deep(.el-table) {
+  font-size: 13px;
+}
+.num-cell {
+  font-family: ui-monospace, 'Cascadia Code', monospace;
+  font-size: 12px;
+}
+
+/* 人体模型 */
+.body-canvas-wrap {
+  display: flex;
+  justify-content: center;
+  padding: 8px;
+  background: linear-gradient(145deg, #fff 0%, #f4f6f9 100%);
+  border-radius: 18px;
+  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 12px 40px -12px rgba(15, 23, 42, 0.10);
+  border: 1px solid rgba(255, 255, 255, 0.8);
+}
+.body-svg {
+  display: block;
+  width: 100%;
+  max-width: 420px;
+  border-radius: 14px;
+}
+
+/* 图例 */
+.marker-legend {
+  margin-top: 16px;
+}
+.legend-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+.legend-title {
+  font-size: 14px;
+  font-weight: 700;
+  color: #1e293b;
+}
+.legend-chips {
+  display: flex;
+  gap: 8px;
+}
+.chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 11px;
+  color: #64748b;
+  padding: 3px 10px;
+  border-radius: 999px;
+  background: #f1f5f9;
+  border: 1px solid #e2e8f0;
+}
+.dot {
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  display: inline-block;
+}
+.dot-step { background: #10b981; }
+.dot-coord { background: #6366f1; }
+
+.legend-cards {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 10px 14px;
+  background: #fafbfc;
+  border-radius: 10px;
+  border: 1px solid #eef0f4;
+  border-left: 4px solid;
+  transition: background 0.15s;
+}
+.legend-item:hover {
+  background: #f1f5f9;
+}
+.legend-num {
+  width: 26px;
+  height: 26px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  font-size: 12px;
+  font-weight: 700;
+  flex-shrink: 0;
+}
+.legend-info {
+  flex: 1;
+  min-width: 0;
+}
+.legend-name {
+  font-size: 13px;
+  font-weight: 600;
+  color: #1e293b;
+}
+.legend-detail {
+  font-size: 11px;
+  color: #64748b;
+  margin-top: 2px;
+}
+
+/* JSON */
+.json-block {
+  background: #1e1e1e;
+  color: #d4d4d4;
+  padding: 16px;
+  border-radius: 10px;
+  overflow: auto;
+  max-height: 400px;
+  font-size: 12px;
+  line-height: 1.5;
+  font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;
+}
+
+.modern-table {
+  border-radius: 8px;
+  overflow: hidden;
+}
+.modern-table :deep(th.el-table__cell) {
+  background-color: #f8fafc;
+  color: #475569;
+  font-weight: 600;
+  font-size: 12px;
+}
+
+.visual-sub {
+  font-size: 13px;
+  color: #64748b;
+  font-weight: 400;
+}
+
+@media (max-width: 1100px) {
+  .sim-layout {
+    flex-direction: column;
+  }
+  .sim-config {
+    flex: 1 1 auto;
+    min-width: 0;
+    max-width: 100%;
+  }
+}
+</style>

+ 133 - 0
openspec/aijiuyiBackend/admin-ui/src/views/UserDevicesView.vue

@@ -0,0 +1,133 @@
+<template>
+  <div>
+    <el-page-header @back="goBack" style="margin-bottom: 20px">
+      <template #content><span class="page-title">用户设备管理 - {{ userName }}</span></template>
+    </el-page-header>
+
+    <el-card shadow="never" class="section-card">
+      <div class="toolbar">
+        <el-button type="primary" @click="showBindDialog = true">添加设备</el-button>
+      </div>
+
+      <el-table :data="devices" class="modern-table" v-loading="loading" style="width: 100%">
+        <el-table-column label="设备编号" width="120">
+          <template #default="{ row }">{{ row.device?.deviceCode }}</template>
+        </el-table-column>
+        <el-table-column label="设备名称" min-width="140">
+          <template #default="{ row }">{{ row.device?.deviceName }}</template>
+        </el-table-column>
+        <el-table-column label="设备型号" width="100">
+          <template #default="{ row }">{{ row.device?.model }}</template>
+        </el-table-column>
+        <el-table-column label="绑定时间" width="170">
+          <template #default="{ row }">{{ row.binding?.boundAt }}</template>
+        </el-table-column>
+        <el-table-column label="是否主设备" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.binding?.isPrimary === 1 ? 'success' : 'info'" size="small">
+              {{ row.binding?.isPrimary === 1 ? '是' : '否' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="在线状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.device?.status === 'online' ? 'success' : 'danger'" size="small">
+              {{ row.device?.status }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="setPrimary(row)">设为主设备</el-button>
+            <el-button link type="danger" @click="unbind(row)">解绑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <!-- 绑定设备弹窗 -->
+    <el-dialog v-model="showBindDialog" title="添加设备" width="500px">
+      <el-form label-width="80px">
+        <el-form-item label="选择设备">
+          <el-select v-model="bindDeviceId" filterable placeholder="搜索设备" style="width: 100%">
+            <el-option v-for="d in allDevices" :key="d.id" :label="`${d.deviceCode} - ${d.deviceName}`" :value="d.id" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showBindDialog = false">取消</el-button>
+        <el-button type="primary" @click="bindDevice">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import http from '../api/http'
+
+const route = useRoute()
+const router = useRouter()
+const userId = route.params.userId
+const userName = ref('')
+const devices = ref([])
+const loading = ref(false)
+const showBindDialog = ref(false)
+const bindDeviceId = ref(null)
+const allDevices = ref([])
+
+async function fetchDevices() {
+  loading.value = true
+  try {
+    const res = await http.get(`/users/${userId}/devices`)
+    devices.value = res.data || []
+  } finally {
+    loading.value = false
+  }
+}
+
+async function fetchUser() {
+  const res = await http.get(`/users/${userId}`)
+  userName.value = res.data?.name || userId
+}
+
+async function fetchAllDevices() {
+  const res = await http.get('/devices', { params: { current: 1, size: 1000 } })
+  allDevices.value = res.data?.records || []
+}
+
+async function bindDevice() {
+  if (!bindDeviceId.value) return
+  await http.post(`/users/${userId}/devices/${bindDeviceId.value}/bind`)
+  ElMessage.success('绑定成功')
+  showBindDialog.value = false
+  await fetchDevices()
+}
+
+async function unbind(row) {
+  await ElMessageBox.confirm('确定解绑此设备?', '确认', { type: 'warning' })
+  await http.delete(`/users/${userId}/devices/${row.binding.deviceId}/unbind`)
+  ElMessage.success('解绑成功')
+  await fetchDevices()
+}
+
+async function setPrimary(row) {
+  await http.put(`/users/${userId}/devices/${row.binding.deviceId}/primary`)
+  ElMessage.success('已设为主设备')
+  await fetchDevices()
+}
+
+function goBack() { router.push('/crud/users') }
+
+onMounted(() => { fetchUser(); fetchDevices(); fetchAllDevices() })
+</script>
+
+<style scoped>
+.page-title { font-size: 18px; font-weight: 600; color: #334155; }
+.section-card { border: none; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,.02); }
+.toolbar { display: flex; gap: 12px; margin-bottom: 16px; }
+.modern-table { border-radius: 8px; overflow: hidden; }
+.modern-table :deep(th.el-table__cell) { background-color: #f8fafc; color: #475569; font-weight: 600; }
+</style>

+ 149 - 124
openspec/aijiuyiBackend/admin-ui/src/views/resource/CrudView.vue

@@ -1,54 +1,69 @@
 <template>
   <el-card v-if="cfg" shadow="never" class="crud-card">
-    <div class="toolbar">
-      <el-button type="primary" @click="openCreate" class="btn-create">新建数据</el-button>
-      <div v-if="cfg.chairProductFilter" class="filter">
-        <span>艾灸椅穴位:</span>
-        <el-select v-model="chairFilter" clearable placeholder="全部" style="width: 120px" @change="onFilterChange">
-          <el-option label="是" :value="true" />
-          <el-option label="否" :value="false" />
+    <!-- 搜索条件 -->
+    <div v-if="cfg.searchFields" class="search-bar">
+      <template v-for="sf in cfg.searchFields" :key="sf.prop">
+        <el-input v-if="sf.type === 'input'" v-model="searchForm[sf.prop]" :placeholder="sf.label"
+          clearable style="width: 160px" @keyup.enter="doSearch" />
+        <el-select v-else-if="sf.type === 'select'" v-model="searchForm[sf.prop]" :placeholder="sf.label"
+          clearable style="width: 140px">
+          <el-option v-for="o in sf.options" :key="o" :label="o || '全部'" :value="o" />
         </el-select>
-      </div>
+      </template>
+      <el-button type="primary" @click="doSearch">查询</el-button>
+      <el-button @click="resetSearch">重置</el-button>
+    </div>
+
+    <!-- 工具栏 -->
+    <div class="toolbar">
+      <el-button type="primary" @click="openCreate" class="btn-create">新增</el-button>
+      <el-button v-if="cfg.batchDeleteApiPath" type="danger" :disabled="!selected.length" @click="batchDelete">批量删除</el-button>
+      <el-button v-if="cfg.extraActions?.includes('publish')" :disabled="!selected.length" @click="batchPublish">批量发布</el-button>
+      <el-button v-if="cfg.extraActions?.includes('archive')" :disabled="!selected.length" @click="batchArchive">批量下架</el-button>
+      <el-button v-if="cfg.exportApiPath" @click="doExport">导出</el-button>
     </div>
-    <el-table :data="rows" class="modern-table" v-loading="loading" style="width: 100%">
-      <el-table-column
-        v-for="col in cfg.columns"
-        :key="col.prop"
-        :prop="col.prop"
-        :label="col.label"
-        :width="col.width"
-        :min-width="col.minWidth"
-        :formatter="col.prop === 'chairProduct' ? formatChair : undefined"
-      />
-      <el-table-column label="操作" width="160" fixed="right">
+
+    <!-- 数据表格 -->
+    <el-table :data="rows" class="modern-table" v-loading="loading" style="width: 100%"
+      @selection-change="onSelectionChange">
+      <el-table-column type="selection" width="50" />
+      <el-table-column v-for="col in cfg.columns" :key="col.prop" :prop="col.prop" :label="col.label"
+        :width="col.width" :min-width="col.minWidth" />
+      <el-table-column label="操作" :width="actionWidth" fixed="right">
         <template #default="{ row }">
           <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+          <el-button v-if="cfg.extraActions?.includes('viewDetail')" link type="primary"
+            @click="viewDetail(row)">详情</el-button>
+          <el-button v-if="cfg.extraActions?.includes('generateAcupoint')" link type="success"
+            @click="generateAcupoint(row)">生成穴位</el-button>
+          <el-button v-if="cfg.extraActions?.includes('manageDevices')" link type="warning"
+            @click="manageDevices(row)">设备</el-button>
+          <el-button v-if="cfg.extraActions?.includes('manageUsers')" link type="warning"
+            @click="manageUsers(row)">用户</el-button>
           <el-button link type="danger" @click="remove(row)">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
-    <el-pagination
-      class="pager"
-      background
-      layout="total, prev, pager, next, sizes"
-      :total="total"
-      v-model:current-page="page.current"
-      v-model:page-size="page.size"
-      :page-sizes="[10, 20, 50]"
-      @current-change="fetchList"
-      @size-change="fetchList"
-    />
+
+    <el-pagination class="pager" background layout="total, prev, pager, next, sizes"
+      :total="total" v-model:current-page="page.current" v-model:page-size="page.size"
+      :page-sizes="[10, 20, 50]" @current-change="fetchList" @size-change="fetchList" />
+
+    <!-- 新增/编辑弹窗 -->
     <el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" destroy-on-close @closed="onDialogClose">
-      <el-form ref="formRef" :model="form" :rules="formRules" label-width="120px" class="edit-form">
-        <el-form-item
-          v-for="f in cfg.formFields"
-          :key="f.prop"
-          :label="f.label"
-          :prop="f.prop"
-        >
+      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="edit-form">
+        <el-form-item v-for="f in cfg.formFields" :key="f.prop" :label="f.label" :prop="f.prop">
           <el-input v-if="f.type === 'input'" v-model="form[f.prop]" />
           <el-input v-else-if="f.type === 'textarea'" v-model="form[f.prop]" type="textarea" :rows="4" />
           <el-input-number v-else-if="f.type === 'number'" v-model="form[f.prop]" :controls="true" style="width: 100%" />
+          <el-select v-else-if="f.type === 'select'" v-model="form[f.prop]" clearable style="width: 100%">
+            <template v-if="Array.isArray(f.options) && typeof f.options[0] === 'object'">
+              <el-option v-for="o in f.options" :key="o.value" :label="o.label" :value="o.value" />
+            </template>
+            <template v-else>
+              <el-option v-for="o in f.options" :key="o" :label="o" :value="o" />
+            </template>
+          </el-select>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -62,16 +77,13 @@
 
 <script setup>
 import { computed, reactive, ref, watch } from 'vue'
-
-function formatChair(row) {
-  return row.chairProduct === 1 ? '是' : '否'
-}
-import { useRoute } from 'vue-router'
-import { ElMessageBox } from 'element-plus'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import http from '../../api/http'
 import { getResourceConfig } from '../../config/resources'
 
 const route = useRoute()
+const router = useRouter()
 const key = computed(() => route.params.resource)
 const cfg = computed(() => getResourceConfig(key.value))
 
@@ -79,16 +91,26 @@ const rows = ref([])
 const total = ref(0)
 const loading = ref(false)
 const page = reactive({ current: 1, size: 10 })
-const chairFilter = ref(undefined)
+const selected = ref([])
+const searchForm = reactive({})
 
 const dialogVisible = ref(false)
 const dialogMode = ref('create')
-const dialogTitle = computed(() => (dialogMode.value === 'create' ? '新' : '编辑'))
+const dialogTitle = computed(() => (dialogMode.value === 'create' ? '新' : '编辑'))
 const saving = ref(false)
 const formRef = ref()
 const form = reactive({})
 const detailBackup = ref(null)
 
+const actionWidth = computed(() => {
+  let w = 120
+  const ea = cfg.value?.extraActions || []
+  if (ea.includes('viewDetail')) w += 50
+  if (ea.includes('generateAcupoint')) w += 70
+  if (ea.includes('manageDevices') || ea.includes('manageUsers')) w += 50
+  return w
+})
+
 const formRules = computed(() => {
   const r = {}
   if (!cfg.value) return r
@@ -108,20 +130,29 @@ function emptyForm() {
   }
 }
 
-function onFilterChange() {
+function resetSearch() {
+  for (const k of Object.keys(searchForm)) searchForm[k] = ''
   page.current = 1
   fetchList()
 }
 
+function doSearch() {
+  page.current = 1
+  fetchList()
+}
+
+function onSelectionChange(val) {
+  selected.value = val
+}
+
 async function fetchList() {
   if (!cfg.value) return
   loading.value = true
   try {
-    const params = { current: page.current, size: page.size }
-    if (cfg.value.chairProductFilter && chairFilter.value !== undefined && chairFilter.value !== null) {
-      params.chairProduct = chairFilter.value
-    }
-    const res = await http.get(cfg.value.apiPath, { params })
+    const hasSearch = cfg.value.searchApiPath && Object.values(searchForm).some(v => v !== '' && v !== undefined && v !== null)
+    const url = hasSearch ? cfg.value.searchApiPath : cfg.value.apiPath
+    const params = { current: page.current, size: page.size, ...searchForm }
+    const res = await http.get(url, { params })
     const pr = res.data || {}
     rows.value = pr.records || []
     total.value = pr.total || 0
@@ -145,9 +176,7 @@ async function openEdit(row) {
   emptyForm()
   for (const f of cfg.value.formFields) {
     let v = d[f.prop]
-    if (f.type === 'number' && v !== null && v !== undefined && v !== '') {
-      v = Number(v)
-    }
+    if (f.type === 'number' && v !== null && v !== undefined && v !== '') v = Number(v)
     form[f.prop] = v ?? (f.type === 'number' ? undefined : '')
   }
   dialogVisible.value = true
@@ -176,9 +205,9 @@ async function save() {
     if (dialogMode.value === 'create') {
       await http.post(cfg.value.apiPath, buildPayload())
     } else {
-      const id = detailBackup.value.id
-      await http.put(`${cfg.value.apiPath}/${id}`, buildPayload())
+      await http.put(`${cfg.value.apiPath}/${detailBackup.value.id}`, buildPayload())
     }
+    ElMessage.success('保存成功')
     dialogVisible.value = false
     await fetchList()
   } finally {
@@ -189,85 +218,81 @@ async function save() {
 async function remove(row) {
   await ElMessageBox.confirm(`确定删除 ID ${row.id}?`, '确认', { type: 'warning' })
   await http.delete(`${cfg.value.apiPath}/${row.id}`)
+  ElMessage.success('删除成功')
   await fetchList()
 }
 
-function onDialogClose() {
-  detailBackup.value = null
+async function batchDelete() {
+  await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 条记录?`, '确认', { type: 'warning' })
+  await http.post(cfg.value.batchDeleteApiPath, { ids: selected.value.map(r => r.id) })
+  ElMessage.success('批量删除成功')
+  await fetchList()
 }
 
-watch(
-  key,
-  () => {
-    chairFilter.value = undefined
-    page.current = 1
-    if (cfg.value) {
-      fetchList()
-    }
-  },
-  { immediate: true }
-)
-</script>
-
-<style scoped>
-.crud-card {
-  min-height: 400px;
-  border: none;
-  border-radius: 12px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
-  padding: 4px;
-}
-.toolbar {
-  display: flex;
-  align-items: center;
-  gap: 16px;
-  margin-bottom: 20px;
-  padding: 0 4px;
-}
-.btn-create {
-  border-radius: 6px;
-  padding: 8px 20px;
-  font-weight: 500;
-  box-shadow: 0 2px 6px rgba(16, 185, 129, 0.2);
-}
-.filter {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 14px;
-  color: #475569;
+async function batchPublish() {
+  await ElMessageBox.confirm(`确定批量发布选中的 ${selected.value.length} 条方案?`, '确认')
+  await http.post('/schemes/batch-publish', { ids: selected.value.map(r => r.id) })
+  ElMessage.success('批量发布成功')
+  await fetchList()
 }
-.pager {
-  margin-top: 24px;
-  justify-content: flex-end;
-  padding-right: 4px;
+
+async function batchArchive() {
+  await ElMessageBox.confirm(`确定批量下架选中的 ${selected.value.length} 条方案?`, '确认')
+  await http.post('/schemes/batch-archive', { ids: selected.value.map(r => r.id) })
+  ElMessage.success('批量下架成功')
+  await fetchList()
 }
-.edit-form {
-  max-height: 60vh;
-  overflow-y: auto;
-  padding-right: 16px;
+
+function doExport() {
+  const params = new URLSearchParams(searchForm)
+  const token = localStorage.getItem('admin_token')
+  const url = `/api${cfg.value.exportApiPath}?${params.toString()}`
+  const a = document.createElement('a')
+  a.href = url
+  a.download = ''
+  a.click()
 }
 
-/* Modern Table Overrides */
-.modern-table {
-  border-radius: 8px;
-  overflow: hidden;
+async function generateAcupoint(row) {
+  await ElMessageBox.confirm(`确定为用户 ${row.name} 生成穴位坐标?`, '确认')
+  await http.post(`/users/${row.id}/acupoint-coordinates`)
+  ElMessage.success('穴位坐标生成成功')
+  await fetchList()
 }
-.modern-table :deep(.el-table__inner-wrapper::before) {
-  display: none;
+
+function manageDevices(row) {
+  router.push({ name: 'user-devices', params: { userId: row.id } })
 }
-.modern-table :deep(th.el-table__cell) {
-  background-color: #f8fafc;
-  color: #475569;
-  font-weight: 600;
-  border-bottom: 1px solid #e2e8f0;
-  padding: 12px 0;
+
+function manageUsers(row) {
+  router.push({ name: 'device-users', params: { deviceId: row.id } })
 }
-.modern-table :deep(td.el-table__cell) {
-  border-bottom: 1px solid #f1f5f9;
-  padding: 12px 0;
+
+function viewDetail(row) {
+  router.push({ name: 'scheme-detail', params: { schemeId: row.id } })
 }
-.modern-table :deep(.el-table__row:hover > td.el-table__cell) {
-  background-color: #f8fafc;
+
+function onDialogClose() {
+  detailBackup.value = null
 }
-</style>
+
+watch(key, () => {
+  for (const k of Object.keys(searchForm)) searchForm[k] = ''
+  page.current = 1
+  if (cfg.value) fetchList()
+}, { immediate: true })
+</script>
+
+<style scoped>
+.crud-card { min-height: 400px; border: none; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,.02); padding: 4px; }
+.search-bar { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; padding: 0 4px; align-items: center; }
+.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; padding: 0 4px; }
+.btn-create { border-radius: 6px; padding: 8px 20px; font-weight: 500; box-shadow: 0 2px 6px rgba(16,185,129,.2); }
+.pager { margin-top: 24px; justify-content: flex-end; padding-right: 4px; }
+.edit-form { max-height: 60vh; overflow-y: auto; padding-right: 16px; }
+.modern-table { border-radius: 8px; overflow: hidden; }
+.modern-table :deep(.el-table__inner-wrapper::before) { display: none; }
+.modern-table :deep(th.el-table__cell) { background-color: #f8fafc; color: #475569; font-weight: 600; border-bottom: 1px solid #e2e8f0; padding: 12px 0; }
+.modern-table :deep(td.el-table__cell) { border-bottom: 1px solid #f1f5f9; padding: 12px 0; }
+.modern-table :deep(.el-table__row:hover > td.el-table__cell) { background-color: #f8fafc; }
+</style>

+ 515 - 0
openspec/aijiuyiBackend/data.sql

@@ -0,0 +1,515 @@
+-- =============================================================================
+-- 智能穴位定位系统 - 数据库重建脚本
+-- 说明:DROP 所有旧表,全新创建 17 张表,插入种子数据。
+-- ID 策略:应用层使用雪花算法生成,种子数据使用固定小 ID(不会冲突)。
+-- =============================================================================
+
+CREATE DATABASE IF NOT EXISTS `aijiuyi_admin`
+  DEFAULT CHARACTER SET utf8mb4
+  COLLATE utf8mb4_unicode_ci;
+USE `aijiuyi_admin`;
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- -----------------------------------------------------------------------------
+-- DROP 所有旧表
+-- -----------------------------------------------------------------------------
+DROP TABLE IF EXISTS `scheme_target_user`;
+DROP TABLE IF EXISTS `moxa_session_record`;
+DROP TABLE IF EXISTS `moxa_process`;
+DROP TABLE IF EXISTS `device_hotkey_mapping`;
+DROP TABLE IF EXISTS `user_acupoint_item`;
+DROP TABLE IF EXISTS `user_acupoint_table`;
+DROP TABLE IF EXISTS `user_scheme`;
+DROP TABLE IF EXISTS `scheme_step`;
+DROP TABLE IF EXISTS `scheme`;
+DROP TABLE IF EXISTS `moxa_technique`;
+DROP TABLE IF EXISTS `acupoint`;
+DROP TABLE IF EXISTS `app_user_device`;
+DROP TABLE IF EXISTS `app_device`;
+DROP TABLE IF EXISTS `app_user_fingerprint`;
+DROP TABLE IF EXISTS `app_user`;
+DROP TABLE IF EXISTS `sys_operation_log`;
+DROP TABLE IF EXISTS `sys_admin_user`;
+
+-- 旧表清理
+DROP TABLE IF EXISTS `admin_user_role`;
+DROP TABLE IF EXISTS `admin_user`;
+DROP TABLE IF EXISTS `admin_role`;
+DROP TABLE IF EXISTS `operation_log`;
+DROP TABLE IF EXISTS `end_user`;
+DROP TABLE IF EXISTS `user_fingerprint`;
+DROP TABLE IF EXISTS `account`;
+DROP TABLE IF EXISTS `moxa_session_record`;
+DROP TABLE IF EXISTS `algorithm_version_record`;
+DROP TABLE IF EXISTS `algorithm_parameter_set`;
+DROP TABLE IF EXISTS `device_model`;
+DROP TABLE IF EXISTS `technique_combo_rule`;
+DROP TABLE IF EXISTS `technique`;
+DROP TABLE IF EXISTS `efficacy_version`;
+DROP TABLE IF EXISTS `efficacy`;
+DROP TABLE IF EXISTS `body_region`;
+DROP TABLE IF EXISTS `symptom`;
+
+-- =============================================================================
+-- 一、系统管理表
+-- =============================================================================
+
+CREATE TABLE `sys_admin_user` (
+    `id`             BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `username`       VARCHAR(64)  NOT NULL COMMENT '登录用户名',
+    `password_hash`  VARCHAR(255) NOT NULL COMMENT 'BCrypt 密码哈希',
+    `display_name`   VARCHAR(128) NULL     COMMENT '姓名',
+    `role`           VARCHAR(32)  NOT NULL DEFAULT 'OPERATOR' COMMENT '角色:SUPER_ADMIN / OPERATOR / READONLY',
+    `status`         VARCHAR(32)  NOT NULL DEFAULT 'ACTIVE'   COMMENT '状态:ACTIVE / DISABLED',
+    `last_login_at`  DATETIME     NULL     COMMENT '最后登录时间',
+    `deleted`        TINYINT      NOT NULL DEFAULT 0,
+    `create_time`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_username` (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台管理员表';
+
+CREATE TABLE `sys_operation_log` (
+    `id`             BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `operator_id`    BIGINT       NULL     COMMENT '操作人 ID',
+    `operator_name`  VARCHAR(64)  NULL     COMMENT '操作人姓名',
+    `action_type`    VARCHAR(32)  NOT NULL COMMENT '操作类型:CREATE / UPDATE / DELETE / IMPORT / EXPORT',
+    `module`         VARCHAR(64)  NOT NULL COMMENT '操作模块:USER / ACUPOINT / SCHEME / DEVICE / SYSTEM',
+    `content`        VARCHAR(512) NULL     COMMENT '操作内容',
+    `ip`             VARCHAR(64)  NULL     COMMENT '客户端 IP',
+    `create_time`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_operator` (`operator_id`),
+    KEY `idx_time` (`create_time`),
+    KEY `idx_module` (`module`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作审计日志表';
+
+-- =============================================================================
+-- 二、用户管理表
+-- =============================================================================
+
+CREATE TABLE `app_user` (
+    `id`                     BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `name`                   VARCHAR(128) NOT NULL COMMENT '用户姓名',
+    `gender`                 VARCHAR(16)  NOT NULL COMMENT '性别:male / female',
+    `age`                    INT          NOT NULL COMMENT '年龄',
+    `phone`                  VARCHAR(32)  NOT NULL COMMENT '手机号(唯一)',
+    `shoulder_width`         DECIMAL(6,2) NOT NULL COMMENT '肩宽(cm)',
+    `body_length`            DECIMAL(6,2) NOT NULL COMMENT '身长/坐高(cm)',
+    `finger_cun1`            DECIMAL(6,2) NULL     COMMENT '一寸(cm),选填',
+    `finger_cun1_5`          DECIMAL(6,2) NULL     COMMENT '1.5寸(cm),选填',
+    `finger_cun3`            DECIMAL(6,2) NULL     COMMENT '三寸(cm),选填',
+    `acupoint_table_status`  VARCHAR(16)  NOT NULL DEFAULT 'NOT_GENERATED' COMMENT '穴位表状态:GENERATED / NOT_GENERATED',
+    `deleted`                TINYINT      NOT NULL DEFAULT 0,
+    `create_time`            DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`            DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_phone` (`phone`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='APP 用户表';
+
+CREATE TABLE `app_user_fingerprint` (
+    `id`              BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `user_id`         BIGINT       NOT NULL COMMENT '用户 ID',
+    `fingerprint_id`  VARCHAR(128) NOT NULL COMMENT '设备侧指纹标识',
+    `deleted`         TINYINT      NOT NULL DEFAULT 0,
+    `create_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户指纹表';
+
+-- =============================================================================
+-- 三、设备管理表
+-- =============================================================================
+
+CREATE TABLE `app_device` (
+    `id`                BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `device_code`       VARCHAR(64)  NOT NULL COMMENT '设备编号(如 AJY001)',
+    `device_name`       VARCHAR(128) NOT NULL COMMENT '设备名称',
+    `model`             VARCHAR(64)  NOT NULL COMMENT '设备型号',
+    `serial_number`     VARCHAR(128) NOT NULL COMMENT '设备序列号(唯一标识)',
+    `firmware_version`  VARCHAR(64)  NULL     COMMENT '固件版本',
+    `status`            VARCHAR(16)  NOT NULL DEFAULT 'offline' COMMENT '在线状态:online / offline',
+    `last_online_at`    DATETIME     NULL     COMMENT '最后在线时间',
+    `remark`            VARCHAR(512) NULL     COMMENT '备注',
+    `deleted`           TINYINT      NOT NULL DEFAULT 0,
+    `create_time`       DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`       DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_device_code` (`device_code`),
+    UNIQUE KEY `uk_serial_number` (`serial_number`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备实例表';
+
+CREATE TABLE `app_user_device` (
+    `id`          BIGINT   NOT NULL COMMENT '主键(雪花算法)',
+    `user_id`     BIGINT   NOT NULL COMMENT '用户 ID',
+    `device_id`   BIGINT   NOT NULL COMMENT '设备 ID',
+    `is_primary`  TINYINT  NOT NULL DEFAULT 0 COMMENT '是否主设备:1 是 0 否',
+    `bound_at`    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '绑定时间',
+    `deleted`     TINYINT  NOT NULL DEFAULT 0,
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_user_device` (`user_id`, `device_id`),
+    KEY `idx_device` (`device_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户-设备绑定关系表(多对多)';
+
+-- =============================================================================
+-- 四、穴位管理表
+-- =============================================================================
+
+CREATE TABLE `acupoint` (
+    `id`              BIGINT        NOT NULL COMMENT '主键(雪花算法)',
+    `code`            VARCHAR(64)   NOT NULL COMMENT '穴位编码(如 dazhui)',
+    `name`            VARCHAR(128)  NOT NULL COMMENT '穴位名称(如 大椎穴)',
+    `meridian`        VARCHAR(128)  NULL     COMMENT '所属经络',
+    `efficacy`        VARCHAR(512)  NULL     COMMENT '功效(逗号分隔)',
+    `positioning`     VARCHAR(16)   NOT NULL DEFAULT 'relative' COMMENT '定位方式:relative / absolute',
+    `offset_cun`      DECIMAL(6,2)  NULL     COMMENT '相对大椎穴纵向偏移(寸数)',
+    `cun_type`        VARCHAR(16)   NULL     COMMENT '指寸类型:cun1 / cun1_5 / cun3',
+    `side`            VARCHAR(16)   NOT NULL DEFAULT 'center'  COMMENT '侧别:center / both',
+    `lateral_offset`  DECIMAL(6,2)  NULL     COMMENT '横向偏移(寸数),双侧穴位使用',
+    `absolute_x`      DECIMAL(12,4) NULL     COMMENT '绝对坐标 X(mm)',
+    `absolute_y`      DECIMAL(12,4) NULL     COMMENT '绝对坐标 Y(mm)',
+    `absolute_z`      DECIMAL(12,4) NULL     COMMENT '绝对坐标 Z(mm)',
+    `target_gender`   VARCHAR(16)   NOT NULL DEFAULT 'all' COMMENT '适用性别:all / male / female',
+    `status`          VARCHAR(16)   NOT NULL DEFAULT 'active'  COMMENT '状态:active / pending',
+    `source`          VARCHAR(16)   NOT NULL DEFAULT 'preset'  COMMENT '数据来源:preset / expert',
+    `deleted`         TINYINT       NOT NULL DEFAULT 0,
+    `create_time`     DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`     DATETIME      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_code` (`code`),
+    KEY `idx_status` (`status`),
+    KEY `idx_meridian` (`meridian`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='穴位基础库表';
+
+CREATE TABLE `moxa_technique` (
+    `id`                  BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `code`                VARCHAR(64)  NOT NULL COMMENT '手法编码(如 gentle)',
+    `name`                VARCHAR(128) NOT NULL COMMENT '手法名称(如 温和灸)',
+    `is_default`          TINYINT      NOT NULL DEFAULT 0 COMMENT '是否默认手法:1 是 0 否',
+    `description`         VARCHAR(512) NULL     COMMENT '手法描述',
+    `movement_pattern`    VARCHAR(32)  NOT NULL COMMENT '运动模式:fixed / vertical / circular / point / oneclick',
+    `temp_min`            DECIMAL(4,1) NOT NULL DEFAULT 35.0 COMMENT '温度范围最小值(°C)',
+    `temp_max`            DECIMAL(4,1) NOT NULL DEFAULT 50.0 COMMENT '温度范围最大值(°C)',
+    `duration_min`        INT          NOT NULL DEFAULT 60   COMMENT '时间范围最小值(秒)',
+    `duration_max`        INT          NOT NULL DEFAULT 1800 COMMENT '时间范围最大值(秒)',
+    `movement_amplitude`  DECIMAL(6,2) NULL     COMMENT '运动幅度(mm)',
+    `movement_speed`      DECIMAL(6,2) NULL     COMMENT '运动速度(mm/s)',
+    `status`              VARCHAR(16)  NOT NULL DEFAULT 'active' COMMENT '状态:active / disabled',
+    `remark`              VARCHAR(512) NULL     COMMENT '备注',
+    `deleted`             TINYINT      NOT NULL DEFAULT 0,
+    `create_time`         DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`         DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_code` (`code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='艾灸手法表';
+
+-- =============================================================================
+-- 五、方案管理表
+-- =============================================================================
+
+CREATE TABLE `scheme` (
+    `id`             BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `scheme_name`    VARCHAR(128) NOT NULL COMMENT '方案名称',
+    `mode`           VARCHAR(32)  NOT NULL COMMENT '模式:ONE_CLICK / PROFESSIONAL / CUSTOM / EXPERT',
+    `efficacy_type`  VARCHAR(32)  NULL     COMMENT '功效类型:驱寒/祛湿/祛风/化瘀/活血/化痰/养颜/扶阳',
+    `creator_type`   VARCHAR(16)  NOT NULL COMMENT '创作人类型:admin / user / expert',
+    `creator_id`     VARCHAR(64)  NULL     COMMENT '创作人 ID',
+    `creator_name`   VARCHAR(128) NULL     COMMENT '创作人显示名称',
+    `symptoms`       VARCHAR(512) NULL     COMMENT '适用症状(逗号分隔)',
+    `target_gender`  VARCHAR(16)  NOT NULL DEFAULT 'all' COMMENT '适用性别:all / male / female',
+    `age_min`        INT          NOT NULL DEFAULT 1   COMMENT '适用年龄下限',
+    `age_max`        INT          NOT NULL DEFAULT 120 COMMENT '适用年龄上限',
+    `description`    VARCHAR(512) NULL     COMMENT '方案描述',
+    `is_default`     TINYINT      NOT NULL DEFAULT 0 COMMENT '是否默认方案',
+    `expert_id`      VARCHAR(64)  NULL     COMMENT '专家 ID(延年圣手模式)',
+    `expert_name`    VARCHAR(128) NULL     COMMENT '专家名称(延年圣手模式)',
+    `publish_type`   VARCHAR(16)  NULL     COMMENT '发布类型:public / private(延年圣手模式)',
+    `process_id`     BIGINT       NULL     COMMENT '关联工艺语言 ID',
+    `status`         VARCHAR(16)  NOT NULL DEFAULT 'draft' COMMENT '状态:draft / published / archived',
+    `deleted`        TINYINT      NOT NULL DEFAULT 0,
+    `create_time`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_mode` (`mode`),
+    KEY `idx_status` (`status`),
+    KEY `idx_efficacy` (`efficacy_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='艾灸方案表(四种模式统一存储)';
+
+CREATE TABLE `scheme_step` (
+    `id`              BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `scheme_id`       BIGINT       NOT NULL COMMENT '方案 ID',
+    `step_order`      INT          NOT NULL COMMENT '步骤序号',
+    `acupoint_id`     BIGINT       NOT NULL COMMENT '穴位 ID',
+    `acupoint_name`   VARCHAR(128) NOT NULL COMMENT '穴位名称(冗余)',
+    `side`            VARCHAR(16)  NOT NULL DEFAULT 'center' COMMENT '侧别:center / both / left / right',
+    `temperature`     DECIMAL(4,1) NOT NULL COMMENT '温度(°C)',
+    `duration`        INT          NOT NULL COMMENT '时间(秒)',
+    `technique_id`    BIGINT       NOT NULL COMMENT '手法 ID',
+    `technique_name`  VARCHAR(128) NOT NULL COMMENT '手法名称(冗余)',
+    `remark`          VARCHAR(256) NULL     COMMENT '备注',
+    `deleted`         TINYINT      NOT NULL DEFAULT 0,
+    `create_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_scheme` (`scheme_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='方案治疗步骤表';
+
+CREATE TABLE `user_scheme` (
+    `id`          BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `user_id`     BIGINT       NOT NULL COMMENT '用户 ID',
+    `scheme_id`   BIGINT       NOT NULL COMMENT '方案 ID',
+    `mode`        VARCHAR(32)  NOT NULL COMMENT '模式',
+    `selected_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '选择时间',
+    `deleted`     TINYINT      NOT NULL DEFAULT 0,
+    `create_time` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_user` (`user_id`),
+    KEY `idx_scheme` (`scheme_id`),
+    KEY `idx_user_mode` (`user_id`, `mode`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户-方案关联表';
+
+CREATE TABLE `scheme_target_user` (
+    `id`          BIGINT   NOT NULL COMMENT '主键(雪花算法)',
+    `scheme_id`   BIGINT   NOT NULL COMMENT '方案 ID',
+    `user_id`     BIGINT   NOT NULL COMMENT '目标用户 ID',
+    `deleted`     TINYINT  NOT NULL DEFAULT 0,
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_scheme` (`scheme_id`),
+    KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='方案指定用户表(延年圣手私有发布)';
+
+-- =============================================================================
+-- 六、穴位坐标数据表(用户穴位对照表)
+-- =============================================================================
+
+CREATE TABLE `user_acupoint_table` (
+    `id`              BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `user_id`         BIGINT       NOT NULL COMMENT '用户 ID',
+    `shoulder_width`  DECIMAL(6,2) NOT NULL COMMENT '生成时肩宽(cm)',
+    `body_length`     DECIMAL(6,2) NOT NULL COMMENT '生成时身长(cm)',
+    `finger_cun1`     DECIMAL(6,2) NOT NULL COMMENT '使用的一寸值(cm)',
+    `finger_cun1_5`   DECIMAL(6,2) NOT NULL COMMENT '使用的 1.5 寸值(cm)',
+    `finger_cun3`     DECIMAL(6,2) NOT NULL COMMENT '使用的三寸值(cm)',
+    `is_estimated`    TINYINT      NOT NULL DEFAULT 0 COMMENT '指寸是否为估算:1 是 0 否',
+    `confidence`      DECIMAL(5,2) NULL     COMMENT '置信度(%)',
+    `precision_cm`    DECIMAL(4,1) NULL     COMMENT '预估精度(±cm)',
+    `deleted`         TINYINT      NOT NULL DEFAULT 0,
+    `create_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户穴位对照表';
+
+CREATE TABLE `user_acupoint_item` (
+    `id`              BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `table_id`        BIGINT       NOT NULL COMMENT '对照表 ID',
+    `user_id`         BIGINT       NOT NULL COMMENT '用户 ID',
+    `acupoint_id`     BIGINT       NOT NULL COMMENT '穴位 ID',
+    `acupoint_code`   VARCHAR(64)  NOT NULL COMMENT '穴位编码',
+    `acupoint_name`   VARCHAR(128) NOT NULL COMMENT '穴位名称',
+    `side`            VARCHAR(16)  NOT NULL DEFAULT 'center' COMMENT '侧别:center / left / right',
+    `offset_x`        DECIMAL(8,2) NOT NULL COMMENT '相对大椎穴横向偏移(cm)',
+    `offset_y`        DECIMAL(8,2) NOT NULL COMMENT '相对大椎穴纵向偏移(cm)',
+    `offset_in_cun`   DECIMAL(6,2) NOT NULL COMMENT '偏移寸数',
+    `cun_type`        VARCHAR(16)  NOT NULL COMMENT '指寸类型:cun1 / cun1_5 / cun3',
+    `deleted`         TINYINT      NOT NULL DEFAULT 0,
+    `create_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_table` (`table_id`),
+    KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户穴位坐标明细表';
+
+-- =============================================================================
+-- 七、设备同步表(延年圣手快捷键)
+-- =============================================================================
+
+CREATE TABLE `device_hotkey_mapping` (
+    `id`           BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `user_id`      BIGINT       NOT NULL COMMENT '用户 ID',
+    `device_id`    BIGINT       NOT NULL COMMENT '设备 ID',
+    `slot`         INT          NOT NULL COMMENT '快捷键槽位(1-8)',
+    `scheme_id`    BIGINT       NOT NULL COMMENT '方案 ID',
+    `scheme_name`  VARCHAR(128) NOT NULL COMMENT '方案名称(冗余)',
+    `process_id`   BIGINT       NULL     COMMENT '工艺语言 ID',
+    `synced_at`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '同步时间',
+    `deleted`      TINYINT      NOT NULL DEFAULT 0,
+    `create_time`  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_user_device_slot` (`user_id`, `device_id`, `slot`),
+    KEY `idx_device` (`device_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备快捷键映射表(延年圣手 1-8)';
+
+-- =============================================================================
+-- 八、工艺语言表
+-- =============================================================================
+
+CREATE TABLE `moxa_process` (
+    `id`           BIGINT   NOT NULL COMMENT '主键(雪花算法)',
+    `scheme_id`    BIGINT   NOT NULL COMMENT '方案 ID',
+    `process_data` JSON     NOT NULL COMMENT '工艺语言数据(JSON)',
+    `version`      INT      NOT NULL DEFAULT 1 COMMENT '版本号',
+    `deleted`      TINYINT  NOT NULL DEFAULT 0,
+    `create_time`  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time`  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_scheme` (`scheme_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='艾灸工艺语言表';
+
+-- =============================================================================
+-- 九、艾灸记录表
+-- =============================================================================
+
+CREATE TABLE `moxa_session_record` (
+    `id`          BIGINT       NOT NULL COMMENT '主键(雪花算法)',
+    `user_id`     BIGINT       NOT NULL COMMENT '用户 ID',
+    `device_id`   BIGINT       NULL     COMMENT '设备 ID',
+    `scheme_id`   BIGINT       NULL     COMMENT '方案 ID',
+    `started_at`  DATETIME     NULL     COMMENT '开始时间',
+    `ended_at`    DATETIME     NULL     COMMENT '结束时间',
+    `duration`    INT          NULL     COMMENT '时长(秒)',
+    `rating`      INT          NULL     COMMENT '评分(1-5)',
+    `remark`      VARCHAR(512) NULL     COMMENT '备注',
+    `deleted`     TINYINT      NOT NULL DEFAULT 0,
+    `create_time` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `update_time` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    PRIMARY KEY (`id`),
+    KEY `idx_user` (`user_id`),
+    KEY `idx_scheme` (`scheme_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='艾灸会话记录表';
+
+-- =============================================================================
+-- 种子数据
+-- =============================================================================
+
+-- --------------- 1. 管理员账号 ---------------
+-- 默认管理员密码: admin
+INSERT INTO `sys_admin_user` (`id`, `username`, `password_hash`, `display_name`, `role`, `status`) VALUES
+(1, 'admin', '$2b$10$yW9CwM2u/vqz4CGZsQXPyeiKYtsu4PbAj.vKstKoY4hbFT6I0LiNm', '系统管理员', 'SUPER_ADMIN', 'ACTIVE');
+
+-- --------------- 2. 基础穴位库(12 个) ---------------
+INSERT INTO `acupoint` (`id`, `code`, `name`, `meridian`, `efficacy`, `positioning`, `offset_cun`, `cun_type`, `side`, `lateral_offset`, `target_gender`, `status`, `source`) VALUES
+(101, 'dazhui',      '大椎穴',   '督脉',           '驱寒,祛湿,解表',   'relative', 0,    'cun1', 'center', NULL, 'all', 'active', 'preset'),
+(102, 'shenzhu',     '身柱穴',   '督脉',           '止咳平喘,安神',    'relative', 3,    'cun1', 'center', NULL, 'all', 'active', 'preset'),
+(103, 'zhiyang',     '至阳穴',   '督脉',           '宽胸理气',         'relative', 7,    'cun1', 'center', NULL, 'all', 'active', 'preset'),
+(104, 'mingmen',     '命门穴',   '督脉',           '补肾壮阳',         'relative', 14,   'cun1', 'center', NULL, 'all', 'active', 'preset'),
+(105, 'yaoyangquan', '腰阳关穴', '督脉',           '强腰健肾',         'relative', 17,   'cun1', 'center', NULL, 'all', 'active', 'preset'),
+(106, 'fengmen',     '风门穴',   '足太阳膀胱经',   '祛风解表',         'relative', 1.5,  'cun1', 'both',   1.5,  'all', 'active', 'preset'),
+(107, 'bingfeng',    '秉风穴',   '手太阳小肠经',   '祛风散寒',         'relative', 3,    'cun1', 'both',   3,    'all', 'active', 'preset'),
+(108, 'feishu',      '肺俞穴',   '足太阳膀胱经',   '化痰止咳',         'relative', 3,    'cun1', 'both',   1.5,  'all', 'active', 'preset'),
+(109, 'geshu',       '膈俞穴',   '足太阳膀胱经',   '活血化瘀',         'relative', 7,    'cun1', 'both',   1.5,  'all', 'active', 'preset'),
+(110, 'pishu',       '脾俞穴',   '足太阳膀胱经',   '健脾养血',         'relative', 11,   'cun1', 'both',   1.5,  'all', 'active', 'preset'),
+(111, 'shenshu',     '肾俞穴',   '足太阳膀胱经',   '补肾益气',         'relative', 14,   'cun1', 'both',   1.5,  'all', 'active', 'preset'),
+(112, 'baliao',      '八髎穴',   '足太阳膀胱经',   '调理下焦',         'relative', 16,   'cun1', 'center', NULL, 'all', 'active', 'preset');
+
+-- --------------- 3. 艾灸手法(5 种) ---------------
+INSERT INTO `moxa_technique` (`id`, `code`, `name`, `is_default`, `description`, `movement_pattern`, `temp_min`, `temp_max`, `duration_min`, `duration_max`, `status`) VALUES
+(201, 'oneclick', '一键艾灸', 1, '系统默认工艺,督脉5穴温和灸',       'oneclick', 42.0, 45.0, 1800, 1800, 'active'),
+(202, 'gentle',   '温和灸',   0, '固定不动,常规艾灸,温补',           'fixed',    35.0, 50.0, 60,   1800, 'active'),
+(203, 'pecking',  '雀啄灸',   0, '上下移动,强刺激,祛邪',             'vertical', 35.0, 50.0, 60,   1800, 'active'),
+(204, 'circular', '回旋灸',   0, '圆周运动,温经散寒',                 'circular', 35.0, 50.0, 60,   1800, 'active'),
+(205, 'sparrow',  '麻雀灸',   0, '点状移动,局部强刺激',               'point',    35.0, 50.0, 60,   1800, 'active');
+
+-- --------------- 4. 一键艾灸默认方案 ---------------
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `age_min`, `age_max`, `description`, `is_default`, `status`) VALUES
+(301, '一键艾灸(默认)', 'ONE_CLICK', NULL, 'admin', '1', 'ADMIN', NULL, 'all', 1, 120, '系统默认一键艾灸方案,督脉5穴温和灸,总时长约30分钟', 1, 'published');
+
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`, `remark`) VALUES
+(401, 301, 1, 101, '大椎穴',   'center', 42.0, 300, 202, '温和灸', '基准穴位'),
+(402, 301, 2, 102, '身柱穴',   'center', 43.0, 300, 202, '温和灸', NULL),
+(403, 301, 3, 103, '至阳穴',   'center', 44.0, 360, 202, '温和灸', NULL),
+(404, 301, 4, 104, '命门穴',   'center', 44.0, 420, 202, '温和灸', NULL),
+(405, 301, 5, 105, '腰阳关穴', 'center', 45.0, 420, 202, '温和灸', NULL);
+
+-- --------------- 5. 8 种专业模式默认方案 ---------------
+
+-- 5.1 驱寒模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(302, '驱寒模式', 'PROFESSIONAL', '驱寒', 'admin', '1', 'ADMIN', '怕冷,手脚冰凉', 'all', '温阳散寒方案,针对怕冷、手脚冰凉等寒证', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(411, 302, 1, 101, '大椎穴', 'center', 46.0, 540, 202, '温和灸'),
+(412, 302, 2, 104, '命门穴', 'center', 47.0, 600, 202, '温和灸');
+
+-- 5.2 祛湿模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(303, '祛湿模式', 'PROFESSIONAL', '祛湿', 'admin', '1', 'ADMIN', '身体困重,大便黏腻', 'all', '健脾祛湿方案,针对身体困重、大便黏腻等湿证', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(421, 303, 1, 101, '大椎穴', 'center', 43.0, 360, 202, '温和灸'),
+(422, 303, 2, 110, '脾俞穴', 'both',   44.0, 420, 202, '温和灸'),
+(423, 303, 3, 111, '肾俞穴', 'both',   44.0, 420, 202, '温和灸');
+
+-- 5.3 祛风模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(304, '祛风模式', 'PROFESSIONAL', '祛风', 'admin', '1', 'ADMIN', '感冒,头痛', 'all', '祛风解表方案,针对感冒、头痛等风证', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(431, 304, 1, 101, '大椎穴', 'center', 41.0, 330, 203, '雀啄灸'),
+(432, 304, 2, 106, '风门穴', 'both',   41.0, 330, 203, '雀啄灸'),
+(433, 304, 3, 107, '秉风穴', 'both',   41.0, 330, 203, '雀啄灸');
+
+-- 5.4 化瘀模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(305, '化瘀模式', 'PROFESSIONAL', '化瘀', 'admin', '1', 'ADMIN', '疼痛,淤青', 'all', '活血化瘀方案,针对疼痛、淤青等血瘀证', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(441, 305, 1, 101, '大椎穴', 'center', 44.0, 420, 204, '回旋灸'),
+(442, 305, 2, 109, '膈俞穴', 'both',   44.0, 420, 204, '回旋灸'),
+(443, 305, 3, 112, '八髎穴', 'center', 44.0, 420, 204, '回旋灸');
+
+-- 5.5 活血模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(306, '活血模式', 'PROFESSIONAL', '活血', 'admin', '1', 'ADMIN', '血液循环不畅', 'all', '活血通络方案,针对血液循环不畅', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(451, 306, 1, 101, '大椎穴', 'center', 43.0, 420, 202, '温和灸'),
+(452, 306, 2, 109, '膈俞穴', 'both',   44.0, 420, 202, '温和灸'),
+(453, 306, 3, 104, '命门穴', 'center', 44.0, 420, 202, '温和灸');
+
+-- 5.6 化痰模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(307, '化痰模式', 'PROFESSIONAL', '化痰', 'admin', '1', 'ADMIN', '咳嗽,痰多', 'all', '化痰止咳方案,针对咳嗽、痰多', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(461, 307, 1, 101, '大椎穴', 'center', 41.0, 360, 202, '温和灸'),
+(462, 307, 2, 108, '肺俞穴', 'both',   42.0, 360, 202, '温和灸'),
+(463, 307, 3, 110, '脾俞穴', 'both',   42.0, 360, 202, '温和灸');
+
+-- 5.7 养颜模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(308, '养颜模式', 'PROFESSIONAL', '养颜', 'admin', '1', 'ADMIN', '皮肤暗沉', 'all', '美容养颜方案,针对皮肤暗沉', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(471, 308, 1, 101, '大椎穴', 'center', 41.0, 330, 202, '温和灸'),
+(472, 308, 2, 108, '肺俞穴', 'both',   41.0, 330, 202, '温和灸'),
+(473, 308, 3, 110, '脾俞穴', 'both',   41.0, 330, 202, '温和灸'),
+(474, 308, 4, 111, '肾俞穴', 'both',   41.0, 330, 202, '温和灸');
+
+-- 5.8 扶阳模式
+INSERT INTO `scheme` (`id`, `scheme_name`, `mode`, `efficacy_type`, `creator_type`, `creator_id`, `creator_name`, `symptoms`, `target_gender`, `description`, `is_default`, `status`) VALUES
+(309, '扶阳模式', 'PROFESSIONAL', '扶阳', 'admin', '1', 'ADMIN', '腰膝酸软', 'all', '温补肾阳方案,针对腰膝酸软', 1, 'published');
+INSERT INTO `scheme_step` (`id`, `scheme_id`, `step_order`, `acupoint_id`, `acupoint_name`, `side`, `temperature`, `duration`, `technique_id`, `technique_name`) VALUES
+(481, 309, 1, 101, '大椎穴', 'center', 46.0, 540, 202, '温和灸'),
+(482, 309, 2, 104, '命门穴', 'center', 47.0, 540, 202, '温和灸'),
+(483, 309, 3, 103, '至阳穴', 'center', 46.0, 540, 202, '温和灸');
+
+-- --------------- 6. 模拟用户数据 ---------------
+INSERT INTO `app_user` (`id`, `name`, `gender`, `age`, `phone`, `shoulder_width`, `body_length`, `finger_cun1`, `finger_cun1_5`, `finger_cun3`, `acupoint_table_status`) VALUES
+(501, '张三', 'male',   35, '13800001234', 42.00, 85.00, 2.1, 3.2, 6.3, 'GENERATED'),
+(502, '李四', 'female', 33, '13900005678', 36.00, 78.00, NULL, NULL, NULL, 'NOT_GENERATED'),
+(503, '王五', 'male',   62, '13700009012', 40.00, 82.00, NULL, NULL, NULL, 'NOT_GENERATED');
+
+-- --------------- 7. 模拟设备数据 ---------------
+INSERT INTO `app_device` (`id`, `device_code`, `device_name`, `model`, `serial_number`, `firmware_version`, `status`) VALUES
+(601, 'AJY001', '艾灸椅-001', 'AJY-2026', 'SN20260001', 'v1.2.0', 'online'),
+(602, 'AJY002', '艾灸椅-002', 'AJY-2026', 'SN20260002', 'v1.1.0', 'offline');
+
+-- --------------- 8. 用户-设备绑定 ---------------
+INSERT INTO `app_user_device` (`id`, `user_id`, `device_id`, `is_primary`, `bound_at`) VALUES
+(701, 501, 601, 1, '2026-03-15 10:00:00'),
+(702, 501, 602, 0, '2026-03-18 14:00:00'),
+(703, 502, 601, 0, '2026-03-18 14:00:00');
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 6 - 0
openspec/aijiuyiBackend/pom.xml

@@ -76,6 +76,12 @@
             <scope>runtime</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>5.2.5</version>
+        </dependency>
+
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>

+ 2 - 1
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/config/SecurityConfig.java

@@ -58,7 +58,8 @@ public class SecurityConfig {
                 .and()
                 .authorizeRequests()
                 .antMatchers("/", "/favicon.ico").permitAll()
-                .antMatchers("/api/v1/auth/login").permitAll()
+                .antMatchers("/api/auth/login").permitAll()
+                .antMatchers("/api/expert/**").permitAll()
                 .antMatchers(
                         "/swagger-ui/**",
                         "/swagger-ui.html",

+ 12 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/BatchIdsRequest.java

@@ -0,0 +1,12 @@
+package com.aijiuyi.admin.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+@Data
+public class BatchIdsRequest {
+    @NotEmpty(message = "ID列表不能为空")
+    private List<Long> ids;
+}

+ 6 - 4
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/CreateAdminRequest.java

@@ -3,14 +3,16 @@ package com.aijiuyi.admin.dto;
 import lombok.Data;
 
 import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
 
-/** 新建后台管理员请求(明文密码仅用于传输,服务端 BCrypt 后落库)。 */
 @Data
 public class CreateAdminRequest {
-
-    @NotBlank
+    @NotBlank @Size(min = 4, max = 20)
     private String username;
-    @NotBlank
+    @NotBlank @Size(min = 8, max = 20)
     private String password;
+    @NotBlank
     private String displayName;
+    @NotBlank
+    private String role;
 }

+ 0 - 19
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/LocalizationPreviewRequest.java

@@ -1,19 +0,0 @@
-package com.aijiuyi.admin.dto;
-
-import com.aijiuyi.admin.service.algorithm.UserAnthropometryInput;
-import lombok.Data;
-
-import javax.validation.Valid;
-import javax.validation.constraints.NotEmpty;
-import java.util.List;
-
-/** 穴位定位预览请求:用户体态 + 穴位编号列表 + 可选设备型号。 */
-@Data
-public class LocalizationPreviewRequest {
-
-    @Valid
-    private UserAnthropometryInput user;
-    @NotEmpty
-    private List<String> acupointCodes;
-    private String deviceModelCode;
-}

+ 0 - 2
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/LoginRequest.java

@@ -4,10 +4,8 @@ import lombok.Data;
 
 import javax.validation.constraints.NotBlank;
 
-/** 管理员登录请求体。 */
 @Data
 public class LoginRequest {
-
     @NotBlank
     private String username;
     @NotBlank

+ 2 - 5
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/LoginResponse.java

@@ -4,15 +4,12 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import java.util.List;
-
-/** 登录成功返回:JWT、用户名、角色编码列表。 */
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
 public class LoginResponse {
-
     private String token;
     private String username;
-    private List<String> roles;
+    private String displayName;
+    private String role;
 }

+ 49 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/PushDataResponse.java

@@ -0,0 +1,49 @@
+package com.aijiuyi.admin.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class PushDataResponse {
+    private Long userId;
+    private String userName;
+    private BodyDataDTO bodyData;
+    private String mode;
+    private SchemeDataDTO scheme;
+
+    @Data
+    public static class BodyDataDTO {
+        private BigDecimal shoulderWidth;
+        private BigDecimal bodyLength;
+        private FingerWidthDTO fingerWidth;
+    }
+
+    @Data
+    public static class FingerWidthDTO {
+        private BigDecimal cun1;
+        private BigDecimal cun1_5;
+        private BigDecimal cun3;
+    }
+
+    @Data
+    public static class SchemeDataDTO {
+        private Long schemeId;
+        private String schemeName;
+        private List<StepDataDTO> steps;
+    }
+
+    @Data
+    public static class StepDataDTO {
+        private int stepOrder;
+        private String acupointId;
+        private String acupointName;
+        private BigDecimal offsetX;
+        private BigDecimal offsetY;
+        private BigDecimal temperature;
+        private int duration;
+        private String techniqueId;
+        private String techniqueName;
+    }
+}

+ 18 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/SchemeDetailResponse.java

@@ -0,0 +1,18 @@
+package com.aijiuyi.admin.dto;
+
+import com.aijiuyi.admin.entity.MoxaSessionRecord;
+import com.aijiuyi.admin.entity.Scheme;
+import com.aijiuyi.admin.entity.SchemeStep;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class SchemeDetailResponse {
+    private Scheme scheme;
+    private List<SchemeStep> steps;
+    private int totalDurationSeconds;
+    private Map<String, Object> statistics;
+    private List<MoxaSessionRecord> recentRecords;
+}

+ 4 - 3
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/UpdateAdminPasswordRequest.java → openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/UpdatePasswordRequest.java

@@ -3,11 +3,12 @@ package com.aijiuyi.admin.dto;
 import lombok.Data;
 
 import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
 
-/** 超级管理员重置他人密码请求体。 */
 @Data
-public class UpdateAdminPasswordRequest {
-
+public class UpdatePasswordRequest {
     @NotBlank
+    private String oldPassword;
+    @NotBlank @Size(min = 8, max = 20)
     private String newPassword;
 }

+ 17 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/dto/UserModeStorageResponse.java

@@ -0,0 +1,17 @@
+package com.aijiuyi.admin.dto;
+
+import com.aijiuyi.admin.entity.DeviceHotkeyMapping;
+import com.aijiuyi.admin.entity.Scheme;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class UserModeStorageResponse {
+    private Long userId;
+    private Scheme oneClickScheme;
+    private List<Scheme> professionalSchemes;
+    private List<Scheme> customSchemes;
+    private List<Scheme> expertSchemes;
+    private List<DeviceHotkeyMapping> syncedHotkeys;
+}

+ 0 - 24
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Account.java

@@ -1,24 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-import java.time.LocalDateTime;
-
-/** C 端注册账号,映射表 account。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("account")
-public class Account extends BaseEntity {
-
-    private String phone;
-
-    @JsonIgnore
-    private String passwordHash;
-    private String status;
-    private LocalDateTime registeredAt;
-    private Integer boundDeviceCount;
-    private LocalDateTime lastLoginAt;
-}

+ 12 - 20
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Acupoint.java

@@ -6,32 +6,24 @@ import lombok.EqualsAndHashCode;
 
 import java.math.BigDecimal;
 
-/**
- * 标准穴位库(含艾灸椅产品标记、后台基准坐标与轨迹等扩展),映射表 acupoint。
- */
 @Data
 @EqualsAndHashCode(callSuper = true)
 @TableName("acupoint")
 public class Acupoint extends BaseEntity {
 
-    private String serialNo;
     private String code;
     private String name;
-    private String sideType;
     private String meridian;
-    private String bodyRegion;
-    private String regionPartition;
-    private String locationText;
-    private String relativePositionText;
-    private String indications;
-    private String symptomCoverage;
-    private String acupointMethod;
-    private Integer chairProduct;
-    private String remark;
-    private BigDecimal baseCoordX;
-    private BigDecimal baseCoordY;
-    private BigDecimal baseCoordZ;
-    private String heightAdaptFormula;
-    private String trajectoryJson;
-    private String groupTag;
+    private String efficacy;
+    private String positioning;
+    private BigDecimal offsetCun;
+    private String cunType;
+    private String side;
+    private BigDecimal lateralOffset;
+    private BigDecimal absoluteX;
+    private BigDecimal absoluteY;
+    private BigDecimal absoluteZ;
+    private String targetGender;
+    private String status;
+    private String source;
 }

+ 0 - 15
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AdminRole.java

@@ -1,15 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/** 后台角色定义,映射表 admin_role。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("admin_role")
-public class AdminRole extends BaseEntity {
-
-    private String code;
-    private String name;
-}

+ 0 - 16
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AlgorithmParameterSet.java

@@ -1,16 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/** 按设备型号的算法参数集,映射表 algorithm_parameter_set。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("algorithm_parameter_set")
-public class AlgorithmParameterSet extends BaseEntity {
-
-    private Long deviceModelId;
-    private String name;
-    private String paramsJson;
-}

+ 0 - 26
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AlgorithmVersionRecord.java

@@ -1,26 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.FieldFill;
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.time.LocalDateTime;
-
-/** 算法参数版本历史记录,映射表 algorithm_version_record。 */
-@Data
-@TableName("algorithm_version_record")
-public class AlgorithmVersionRecord implements Serializable {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-    private String versionLabel;
-    private String snapshotJson;
-    private String changeNote;
-
-    @TableField(fill = FieldFill.INSERT)
-    private LocalDateTime createdAt;
-}

+ 22 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppDevice.java

@@ -0,0 +1,22 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("app_device")
+public class AppDevice extends BaseEntity {
+
+    private String deviceCode;
+    private String deviceName;
+    private String model;
+    private String serialNumber;
+    private String firmwareVersion;
+    private String status;
+    private LocalDateTime lastOnlineAt;
+    private String remark;
+}

+ 28 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppUser.java

@@ -0,0 +1,28 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("app_user")
+public class AppUser extends BaseEntity {
+
+    private String name;
+    private String gender;
+    private Integer age;
+    private String phone;
+    private BigDecimal shoulderWidth;
+    private BigDecimal bodyLength;
+    private BigDecimal fingerCun1;
+
+    @TableField("finger_cun1_5")
+    private BigDecimal fingerCun1_5;
+
+    private BigDecimal fingerCun3;
+    private String acupointTableStatus;
+}

+ 18 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppUserDevice.java

@@ -0,0 +1,18 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("app_user_device")
+public class AppUserDevice extends BaseEntity {
+
+    private Long userId;
+    private Long deviceId;
+    private Integer isPrimary;
+    private LocalDateTime boundAt;
+}

+ 3 - 4
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/UserFingerprint.java → openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AppUserFingerprint.java

@@ -4,12 +4,11 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
-/** 用户指纹标识,映射表 user_fingerprint。 */
 @Data
 @EqualsAndHashCode(callSuper = true)
-@TableName("user_fingerprint")
-public class UserFingerprint extends BaseEntity {
+@TableName("app_user_fingerprint")
+public class AppUserFingerprint extends BaseEntity {
 
-    private Long endUserId;
+    private Long userId;
     private String fingerprintId;
 }

+ 1 - 1
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/BaseEntity.java

@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
 @Data
 public abstract class BaseEntity implements Serializable {
 
-    @TableId(type = IdType.AUTO)
+    @TableId(type = IdType.ASSIGN_ID)
     private Long id;
 
     @TableLogic

+ 0 - 15
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/BodyRegion.java

@@ -1,15 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/** 身体部位与分区,映射表 body_region。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("body_region")
-public class BodyRegion extends BaseEntity {
-
-    private String bodyPart;
-    private String regionPartition;
-}

+ 21 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/DeviceHotkeyMapping.java

@@ -0,0 +1,21 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("device_hotkey_mapping")
+public class DeviceHotkeyMapping extends BaseEntity {
+
+    private Long userId;
+    private Long deviceId;
+    private Integer slot;
+    private Long schemeId;
+    private String schemeName;
+    private Long processId;
+    private LocalDateTime syncedAt;
+}

+ 0 - 28
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/DeviceModel.java

@@ -1,28 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-import java.math.BigDecimal;
-
-/** 艾灸设备型号(行程、原点、安全距离等),映射表 device_model。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("device_model")
-public class DeviceModel extends BaseEntity {
-
-    private String code;
-    private String name;
-    private BigDecimal strokeRangeX;
-    private BigDecimal strokeRangeY;
-    private BigDecimal strokeRangeZ;
-    private BigDecimal originOffsetX;
-    private BigDecimal originOffsetY;
-    private BigDecimal originOffsetZ;
-    private String axisDirectionJson;
-    private BigDecimal safetyDistance;
-    private BigDecimal standardHeightCm;
-    private BigDecimal standardWeightKg;
-    private BigDecimal standardFingerCunCm;
-}

+ 0 - 21
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Efficacy.java

@@ -1,21 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/** 艾灸功效主表(含灸方 JSON),映射表 efficacy。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("efficacy")
-public class Efficacy extends BaseEntity {
-
-    private String name;
-    private String summary;
-    private String iconUrl;
-    private String applicableCrowd;
-    private String descriptionText;
-    private String status;
-    private String currentVersion;
-    private String prescriptionJson;
-}

+ 0 - 27
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/EndUser.java

@@ -1,27 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-
-/** C 端用户档案(身高、体重、肩宽等算法输入),映射表 end_user。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("end_user")
-public class EndUser extends BaseEntity {
-
-    private Long accountId;
-    private String displayName;
-    private String gender;
-    private Integer age;
-    private BigDecimal heightCm;
-    private BigDecimal weightKg;
-    private BigDecimal shoulderWidthCm;
-    private BigDecimal spineLengthCm;
-    private BigDecimal fingerCunCm;
-    private Integer fingerprintCount;
-    private LocalDateTime lastMoxaAt;
-}

+ 5 - 5
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Symptom.java → openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/MoxaProcess.java

@@ -4,12 +4,12 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
-/** 症状字典,映射表 symptom。 */
 @Data
 @EqualsAndHashCode(callSuper = true)
-@TableName("symptom")
-public class Symptom extends BaseEntity {
+@TableName("moxa_process")
+public class MoxaProcess extends BaseEntity {
 
-    private String code;
-    private String name;
+    private Long schemeId;
+    private String processData;
+    private Integer version;
 }

+ 5 - 3
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/MoxaSessionRecord.java

@@ -6,15 +6,17 @@ import lombok.EqualsAndHashCode;
 
 import java.time.LocalDateTime;
 
-/** 艾灸会话/记录,映射表 moxa_session_record。 */
 @Data
 @EqualsAndHashCode(callSuper = true)
 @TableName("moxa_session_record")
 public class MoxaSessionRecord extends BaseEntity {
 
-    private Long endUserId;
-    private String deviceId;
+    private Long userId;
+    private Long deviceId;
+    private Long schemeId;
     private LocalDateTime startedAt;
     private LocalDateTime endedAt;
+    private Integer duration;
+    private Integer rating;
     private String remark;
 }

+ 27 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/MoxaTechnique.java

@@ -0,0 +1,27 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("moxa_technique")
+public class MoxaTechnique extends BaseEntity {
+
+    private String code;
+    private String name;
+    private Integer isDefault;
+    private String description;
+    private String movementPattern;
+    private BigDecimal tempMin;
+    private BigDecimal tempMax;
+    private Integer durationMin;
+    private Integer durationMax;
+    private BigDecimal movementAmplitude;
+    private BigDecimal movementSpeed;
+    private String status;
+    private String remark;
+}

+ 29 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Scheme.java

@@ -0,0 +1,29 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("scheme")
+public class Scheme extends BaseEntity {
+
+    private String schemeName;
+    private String mode;
+    private String efficacyType;
+    private String creatorType;
+    private String creatorId;
+    private String creatorName;
+    private String symptoms;
+    private String targetGender;
+    private Integer ageMin;
+    private Integer ageMax;
+    private String description;
+    private Integer isDefault;
+    private String expertId;
+    private String expertName;
+    private String publishType;
+    private Long processId;
+    private String status;
+}

+ 24 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SchemeStep.java

@@ -0,0 +1,24 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("scheme_step")
+public class SchemeStep extends BaseEntity {
+
+    private Long schemeId;
+    private Integer stepOrder;
+    private Long acupointId;
+    private String acupointName;
+    private String side;
+    private BigDecimal temperature;
+    private Integer duration;
+    private Long techniqueId;
+    private String techniqueName;
+    private String remark;
+}

+ 11 - 8
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/EfficacyVersion.java → openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SchemeTargetUser.java

@@ -4,23 +4,26 @@ import com.baomidou.mybatisplus.annotation.FieldFill;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
 import java.io.Serializable;
 import java.time.LocalDateTime;
 
-/** 功效历史版本快照,映射表 efficacy_version。 */
 @Data
-@TableName("efficacy_version")
-public class EfficacyVersion implements Serializable {
+@TableName("scheme_target_user")
+public class SchemeTargetUser implements Serializable {
 
-    @TableId(type = IdType.AUTO)
+    @TableId(type = IdType.ASSIGN_ID)
     private Long id;
-    private Long efficacyId;
-    private String versionNo;
-    private String snapshotJson;
+
+    private Long schemeId;
+    private Long userId;
+
+    @TableLogic
+    private Integer deleted;
 
     @TableField(fill = FieldFill.INSERT)
-    private LocalDateTime createdAt;
+    private LocalDateTime createTime;
 }

+ 6 - 4
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/AdminUser.java → openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SysAdminUser.java

@@ -5,16 +5,18 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
-/** 后台管理员账号,映射表 admin_user(密码哈希序列化时忽略)。 */
+import java.time.LocalDateTime;
+
 @Data
 @EqualsAndHashCode(callSuper = true)
-@TableName("admin_user")
-public class AdminUser extends BaseEntity {
+@TableName("sys_admin_user")
+public class SysAdminUser extends BaseEntity {
 
     private String username;
-
     @JsonIgnore
     private String passwordHash;
     private String displayName;
+    private String role;
     private String status;
+    private LocalDateTime lastLoginAt;
 }

+ 9 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/OperationLog.java → openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/SysOperationLog.java

@@ -10,19 +10,18 @@ import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
 
-/** 后台操作审计日志,映射表 operation_log。 */
 @Data
-@TableName("operation_log")
-public class OperationLog implements Serializable {
+@TableName("sys_operation_log")
+public class SysOperationLog implements Serializable {
 
-    @TableId(type = IdType.AUTO)
+    @TableId(type = IdType.ASSIGN_ID)
     private Long id;
-    private Long adminUserId;
-    private String username;
-    private String action;
-    private String resourceType;
-    private String resourceId;
-    private String detail;
+
+    private Long operatorId;
+    private String operatorName;
+    private String actionType;
+    private String module;
+    private String content;
     private String ip;
 
     @TableField(fill = FieldFill.INSERT)

+ 0 - 16
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/Technique.java

@@ -1,16 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/** 灸法/手法定义,映射表 technique。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("technique")
-public class Technique extends BaseEntity {
-
-    private String code;
-    private String name;
-    private String paramsJson;
-}

+ 0 - 15
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/TechniqueComboRule.java

@@ -1,15 +0,0 @@
-package com.aijiuyi.admin.entity;
-
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/** 手法主辅组合规则,映射表 technique_combo_rule。 */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@TableName("technique_combo_rule")
-public class TechniqueComboRule extends BaseEntity {
-
-    private String name;
-    private String logicJson;
-}

+ 24 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/UserAcupointItem.java

@@ -0,0 +1,24 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("user_acupoint_item")
+public class UserAcupointItem extends BaseEntity {
+
+    private Long tableId;
+    private Long userId;
+    private Long acupointId;
+    private String acupointCode;
+    private String acupointName;
+    private String side;
+    private BigDecimal offsetX;
+    private BigDecimal offsetY;
+    private BigDecimal offsetInCun;
+    private String cunType;
+}

+ 25 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/UserAcupointTable.java

@@ -0,0 +1,25 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("user_acupoint_table")
+public class UserAcupointTable extends BaseEntity {
+
+    private Long userId;
+    private BigDecimal shoulderWidth;
+    private BigDecimal bodyLength;
+    private BigDecimal fingerCun1;
+    @TableField("finger_cun1_5")
+    private BigDecimal fingerCun1_5;
+    private BigDecimal fingerCun3;
+    private Integer isEstimated;
+    private BigDecimal confidence;
+    private BigDecimal precisionCm;
+}

+ 18 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/entity/UserScheme.java

@@ -0,0 +1,18 @@
+package com.aijiuyi.admin.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("user_scheme")
+public class UserScheme extends BaseEntity {
+
+    private Long userId;
+    private Long schemeId;
+    private String mode;
+    private LocalDateTime selectedAt;
+}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AccountMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.Account;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** C 端账号表数据访问。 */
-@Mapper
-public interface AccountMapper extends BaseMapper<Account> {
-}

+ 0 - 3
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AcupointMapper.java

@@ -2,9 +2,6 @@ package com.aijiuyi.admin.mapper;
 
 import com.aijiuyi.admin.entity.Acupoint;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
 
-/** 标准穴位表数据访问。 */
-@Mapper
 public interface AcupointMapper extends BaseMapper<Acupoint> {
 }

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AdminRoleMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.AdminRole;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 后台角色表数据访问。 */
-@Mapper
-public interface AdminRoleMapper extends BaseMapper<AdminRole> {
-}

+ 0 - 24
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AdminUserMapper.java

@@ -1,24 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.AdminUser;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-import org.apache.ibatis.annotations.Param;
-import org.apache.ibatis.annotations.Select;
-
-import java.util.List;
-
-/**
- * 后台管理员表数据访问;扩展按用户 ID 查询已授权角色编码列表。
- */
-@Mapper
-public interface AdminUserMapper extends BaseMapper<AdminUser> {
-
-    /**
-     * 查询管理员已绑定的角色编码列表(用于 Spring Security 与 JWT 载荷)。
-     */
-    @Select("SELECT r.code FROM admin_role r "
-            + "INNER JOIN admin_user_role ur ON r.id = ur.admin_role_id "
-            + "WHERE ur.admin_user_id = #{userId} AND r.deleted = 0")
-    List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
-}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AlgorithmParameterSetMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.AlgorithmParameterSet;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 算法参数集表数据访问。 */
-@Mapper
-public interface AlgorithmParameterSetMapper extends BaseMapper<AlgorithmParameterSet> {
-}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AlgorithmVersionRecordMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.AlgorithmVersionRecord;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 算法版本记录表数据访问。 */
-@Mapper
-public interface AlgorithmVersionRecordMapper extends BaseMapper<AlgorithmVersionRecord> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppDeviceMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.AppDevice;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface AppDeviceMapper extends BaseMapper<AppDevice> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppUserDeviceMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.AppUserDevice;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface AppUserDeviceMapper extends BaseMapper<AppUserDevice> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppUserFingerprintMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.AppUserFingerprint;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface AppUserFingerprintMapper extends BaseMapper<AppUserFingerprint> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/AppUserMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.AppUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface AppUserMapper extends BaseMapper<AppUser> {
+}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/BodyRegionMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.BodyRegion;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 身体部位表数据访问。 */
-@Mapper
-public interface BodyRegionMapper extends BaseMapper<BodyRegion> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/DeviceHotkeyMappingMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.DeviceHotkeyMapping;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface DeviceHotkeyMappingMapper extends BaseMapper<DeviceHotkeyMapping> {
+}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/DeviceModelMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.DeviceModel;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 设备型号表数据访问。 */
-@Mapper
-public interface DeviceModelMapper extends BaseMapper<DeviceModel> {
-}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/EfficacyMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.Efficacy;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 功效主表数据访问。 */
-@Mapper
-public interface EfficacyMapper extends BaseMapper<Efficacy> {
-}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/EfficacyVersionMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.EfficacyVersion;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 功效版本表数据访问。 */
-@Mapper
-public interface EfficacyVersionMapper extends BaseMapper<EfficacyVersion> {
-}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/EndUserMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.EndUser;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** C 端用户表数据访问。 */
-@Mapper
-public interface EndUserMapper extends BaseMapper<EndUser> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/MoxaProcessMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.MoxaProcess;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface MoxaProcessMapper extends BaseMapper<MoxaProcess> {
+}

+ 0 - 3
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/MoxaSessionRecordMapper.java

@@ -2,9 +2,6 @@ package com.aijiuyi.admin.mapper;
 
 import com.aijiuyi.admin.entity.MoxaSessionRecord;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
 
-/** 艾灸记录表数据访问。 */
-@Mapper
 public interface MoxaSessionRecordMapper extends BaseMapper<MoxaSessionRecord> {
 }

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/MoxaTechniqueMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.MoxaTechnique;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface MoxaTechniqueMapper extends BaseMapper<MoxaTechnique> {
+}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/OperationLogMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.OperationLog;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 操作日志表数据访问。 */
-@Mapper
-public interface OperationLogMapper extends BaseMapper<OperationLog> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SchemeMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.Scheme;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface SchemeMapper extends BaseMapper<Scheme> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SchemeStepMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.SchemeStep;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface SchemeStepMapper extends BaseMapper<SchemeStep> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SchemeTargetUserMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.SchemeTargetUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface SchemeTargetUserMapper extends BaseMapper<SchemeTargetUser> {
+}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SymptomMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.Symptom;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 症状字典表数据访问。 */
-@Mapper
-public interface SymptomMapper extends BaseMapper<Symptom> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SysAdminUserMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.SysAdminUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface SysAdminUserMapper extends BaseMapper<SysAdminUser> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/SysOperationLogMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.SysOperationLog;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface SysOperationLogMapper extends BaseMapper<SysOperationLog> {
+}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/TechniqueComboRuleMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.TechniqueComboRule;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 手法组合规则表数据访问。 */
-@Mapper
-public interface TechniqueComboRuleMapper extends BaseMapper<TechniqueComboRule> {
-}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/TechniqueMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.Technique;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 手法表数据访问。 */
-@Mapper
-public interface TechniqueMapper extends BaseMapper<Technique> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserAcupointItemMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.UserAcupointItem;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface UserAcupointItemMapper extends BaseMapper<UserAcupointItem> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserAcupointTableMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.UserAcupointTable;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface UserAcupointTableMapper extends BaseMapper<UserAcupointTable> {
+}

+ 0 - 10
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserFingerprintMapper.java

@@ -1,10 +0,0 @@
-package com.aijiuyi.admin.mapper;
-
-import com.aijiuyi.admin.entity.UserFingerprint;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-/** 用户指纹表数据访问。 */
-@Mapper
-public interface UserFingerprintMapper extends BaseMapper<UserFingerprint> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/mapper/UserSchemeMapper.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.UserScheme;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+public interface UserSchemeMapper extends BaseMapper<UserScheme> {
+}

+ 8 - 11
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/security/AdminDetailsServiceImpl.java

@@ -1,31 +1,28 @@
 package com.aijiuyi.admin.security;
 
-import com.aijiuyi.admin.entity.AdminUser;
-import com.aijiuyi.admin.mapper.AdminUserMapper;
+import com.aijiuyi.admin.entity.SysAdminUser;
+import com.aijiuyi.admin.mapper.SysAdminUserMapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 
-import java.util.List;
-
 @Service
 public class AdminDetailsServiceImpl implements AdminDetailsService {
 
-    private final AdminUserMapper adminUserMapper;
+    private final SysAdminUserMapper adminUserMapper;
 
-    public AdminDetailsServiceImpl(AdminUserMapper adminUserMapper) {
+    public AdminDetailsServiceImpl(SysAdminUserMapper adminUserMapper) {
         this.adminUserMapper = adminUserMapper;
     }
 
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
-        AdminUser user = adminUserMapper.selectOne(Wrappers.<AdminUser>lambdaQuery()
-                .eq(AdminUser::getUsername, username));
+        SysAdminUser user = adminUserMapper.selectOne(Wrappers.<SysAdminUser>lambdaQuery()
+                .eq(SysAdminUser::getUsername, username));
         if (user == null) {
-            throw new UsernameNotFoundException(username);
+            throw new UsernameNotFoundException("管理员不存在: " + username);
         }
-        List<String> roles = adminUserMapper.selectRoleCodesByUserId(user.getId());
-        return new AdminUserDetails(user, roles);
+        return new AdminUserDetails(user);
     }
 }

+ 10 - 20
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/security/AdminUserDetails.java

@@ -1,29 +1,25 @@
 package com.aijiuyi.admin.security;
 
-import com.aijiuyi.admin.entity.AdminUser;
+import com.aijiuyi.admin.entity.SysAdminUser;
 import lombok.Getter;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 
-/**
- * Spring Security 用户详情:封装 {@link com.aijiuyi.admin.entity.AdminUser} 与角色(ROLE_ 前缀)。
- */
 @Getter
 public class AdminUserDetails implements UserDetails {
 
-    private final AdminUser user;
+    private final SysAdminUser user;
     private final List<GrantedAuthority> authorities;
 
-    public AdminUserDetails(AdminUser user, List<String> roleCodes) {
+    public AdminUserDetails(SysAdminUser user) {
         this.user = user;
-        this.authorities = roleCodes.stream()
-                .map(c -> new SimpleGrantedAuthority("ROLE_" + c))
-                .collect(Collectors.toList());
+        this.authorities = Collections.singletonList(
+                new SimpleGrantedAuthority("ROLE_" + user.getRole()));
     }
 
     @Override
@@ -42,22 +38,16 @@ public class AdminUserDetails implements UserDetails {
     }
 
     @Override
-    public boolean isAccountNonExpired() {
-        return true;
-    }
+    public boolean isAccountNonExpired() { return true; }
 
     @Override
-    public boolean isAccountNonLocked() {
-        return true;
-    }
+    public boolean isAccountNonLocked() { return true; }
 
     @Override
-    public boolean isCredentialsNonExpired() {
-        return true;
-    }
+    public boolean isCredentialsNonExpired() { return true; }
 
     @Override
     public boolean isEnabled() {
-        return user.getStatus() != null && "ACTIVE".equalsIgnoreCase(user.getStatus());
+        return "ACTIVE".equalsIgnoreCase(user.getStatus());
     }
 }

+ 12 - 16
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/security/OperationLogAspect.java

@@ -1,7 +1,7 @@
 package com.aijiuyi.admin.security;
 
-import com.aijiuyi.admin.entity.OperationLog;
-import com.aijiuyi.admin.service.OpLogService;
+import com.aijiuyi.admin.entity.SysOperationLog;
+import com.aijiuyi.admin.service.SysOperationLogService;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
@@ -14,17 +14,14 @@ import org.springframework.web.context.request.ServletRequestAttributes;
 
 import javax.servlet.http.HttpServletRequest;
 
-/**
- * 记录后台写操作(POST/PUT/PATCH/DELETE)到 operation_log,失败不影响业务主流程。
- */
 @Aspect
 @Component
 public class OperationLogAspect {
 
-    private final OpLogService opLogService;
+    private final SysOperationLogService logService;
 
-    public OperationLogAspect(OpLogService opLogService) {
-        this.opLogService = opLogService;
+    public OperationLogAspect(SysOperationLogService logService) {
+        this.logService = logService;
     }
 
     @Around("execution(* com.aijiuyi.admin.web..*Controller.*(..)) && !execution(* com.aijiuyi.admin.web.AuthController.*(..))")
@@ -40,21 +37,20 @@ public class OperationLogAspect {
         }
         Object result = pjp.proceed();
         try {
-            OperationLog log = new OperationLog();
-            log.setAction(method + " " + request.getRequestURI());
-            log.setResourceType(pjp.getTarget().getClass().getSimpleName());
+            SysOperationLog log = new SysOperationLog();
+            log.setActionType(method);
+            log.setModule(pjp.getTarget().getClass().getSimpleName().replace("Controller", "").toUpperCase());
             MethodSignature sig = (MethodSignature) pjp.getSignature();
-            log.setDetail(sig.getMethod().getName());
+            log.setContent(method + " " + request.getRequestURI() + " - " + sig.getMethod().getName());
             log.setIp(request.getRemoteAddr());
             Authentication auth = SecurityContextHolder.getContext().getAuthentication();
             if (auth != null && auth.getPrincipal() instanceof AdminUserDetails) {
                 AdminUserDetails d = (AdminUserDetails) auth.getPrincipal();
-                log.setAdminUserId(d.getUser().getId());
-                log.setUsername(d.getUsername());
+                log.setOperatorId(d.getUser().getId());
+                log.setOperatorName(d.getUser().getDisplayName());
             }
-            opLogService.save(log);
+            logService.save(log);
         } catch (Exception ignored) {
-            // never break business flow
         }
         return result;
     }

+ 0 - 8
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AccountService.java

@@ -1,8 +0,0 @@
-package com.aijiuyi.admin.service;
-
-import com.aijiuyi.admin.entity.Account;
-import com.baomidou.mybatisplus.extension.service.IService;
-
-/** C 端账号。 */
-public interface AccountService extends IService<Account> {
-}

+ 12 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AcupointCoordinateService.java

@@ -0,0 +1,12 @@
+package com.aijiuyi.admin.service;
+
+import com.aijiuyi.admin.entity.AppUser;
+
+import java.util.Map;
+
+public interface AcupointCoordinateService {
+
+    Map<String, Object> generateCoordinates(AppUser user);
+
+    Map<String, Object> getCoordinates(Long userId);
+}

+ 0 - 1
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AcupointService.java

@@ -3,6 +3,5 @@ package com.aijiuyi.admin.service;
 import com.aijiuyi.admin.entity.Acupoint;
 import com.baomidou.mybatisplus.extension.service.IService;
 
-/** 标准穴位。 */
 public interface AcupointService extends IService<Acupoint> {
 }

+ 0 - 12
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AdminUserService.java

@@ -1,12 +0,0 @@
-package com.aijiuyi.admin.service;
-
-import com.aijiuyi.admin.entity.AdminUser;
-import com.baomidou.mybatisplus.extension.service.IService;
-
-/** 后台管理员:创建用户加密密码、修改密码等。 */
-public interface AdminUserService extends IService<AdminUser> {
-
-    void createWithEncodedPassword(AdminUser user, String rawPassword);
-
-    void updatePassword(Long id, String rawPassword);
-}

+ 0 - 8
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AlgoParamService.java

@@ -1,8 +0,0 @@
-package com.aijiuyi.admin.service;
-
-import com.aijiuyi.admin.entity.AlgorithmParameterSet;
-import com.baomidou.mybatisplus.extension.service.IService;
-
-/** 算法参数集。 */
-public interface AlgoParamService extends IService<AlgorithmParameterSet> {
-}

+ 0 - 8
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AlgoVerService.java

@@ -1,8 +0,0 @@
-package com.aijiuyi.admin.service;
-
-import com.aijiuyi.admin.entity.AlgorithmVersionRecord;
-import com.baomidou.mybatisplus.extension.service.IService;
-
-/** 算法版本记录。 */
-public interface AlgoVerService extends IService<AlgorithmVersionRecord> {
-}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppDeviceService.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.service;
+
+import com.aijiuyi.admin.entity.AppDevice;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+public interface AppDeviceService extends IService<AppDevice> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppUserDeviceService.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.service;
+
+import com.aijiuyi.admin.entity.AppUserDevice;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+public interface AppUserDeviceService extends IService<AppUserDevice> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppUserFingerprintService.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.service;
+
+import com.aijiuyi.admin.entity.AppUserFingerprint;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+public interface AppUserFingerprintService extends IService<AppUserFingerprint> {
+}

+ 7 - 0
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AppUserService.java

@@ -0,0 +1,7 @@
+package com.aijiuyi.admin.service;
+
+import com.aijiuyi.admin.entity.AppUser;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+public interface AppUserService extends IService<AppUser> {
+}

+ 0 - 2
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/AuthService.java

@@ -3,8 +3,6 @@ package com.aijiuyi.admin.service;
 import com.aijiuyi.admin.dto.LoginRequest;
 import com.aijiuyi.admin.dto.LoginResponse;
 
-/** 管理员认证:校验用户名密码并签发 JWT。 */
 public interface AuthService {
-
     LoginResponse login(LoginRequest request);
 }

+ 0 - 8
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/BodyRegionService.java

@@ -1,8 +0,0 @@
-package com.aijiuyi.admin.service;
-
-import com.aijiuyi.admin.entity.BodyRegion;
-import com.baomidou.mybatisplus.extension.service.IService;
-
-/** 身体部位分区。 */
-public interface BodyRegionService extends IService<BodyRegion> {
-}

+ 0 - 8
openspec/aijiuyiBackend/src/main/java/com/aijiuyi/admin/service/ComboRuleService.java

@@ -1,8 +0,0 @@
-package com.aijiuyi.admin.service;
-
-import com.aijiuyi.admin.entity.TechniqueComboRule;
-import com.baomidou.mybatisplus.extension.service.IService;
-
-/** 手法组合规则。 */
-public interface ComboRuleService extends IService<TechniqueComboRule> {
-}

Некоторые файлы не были показаны из-за большого количества измененных файлов