소스 검색

设备绑定用户逻辑变更

jiapu 3 달 전
부모
커밋
c21686e14d
31개의 변경된 파일1825개의 추가작업 그리고 105개의 파일을 삭제
  1. 105 34
      code/ajyApp/pages/device/bodyParams/bodyParams.nvue
  2. 14 0
      code/ajyApp/pages/device/deviceInfo/deviceInfo.nvue
  3. 70 1
      code/ajyApp/pages/device/deviceInfo/mixins/ble-mixin.js
  4. 42 4
      code/ajyApp/pages/device/list/list.nvue
  5. 34 7
      code/ajyApp/pages/device/search/search.nvue
  6. 35 15
      code/ajyApp/pages/device/userManage/userManage.nvue
  7. 18 2
      code/ajyApp/pages/my/info/info.nvue
  8. 3 3
      code/ajyApp/utils/api/user.js
  9. 48 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/AppDeviceController.java
  10. 52 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/AppGroupController.java
  11. 13 39
      code/backend/src/main/java/com/aijiuyi/admin/controller/AppProfileController.java
  12. 16 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppDeviceBindDTO.java
  13. 22 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppDeviceSelectDTO.java
  14. 62 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppGroupMemberDTO.java
  15. 52 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppProfileDTO.java
  16. 3 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/DeviceQueryDTO.java
  17. 3 0
      code/backend/src/main/java/com/aijiuyi/admin/controller/dto/DeviceSaveDTO.java
  18. 3 0
      code/backend/src/main/java/com/aijiuyi/admin/entity/Device.java
  19. 41 0
      code/backend/src/main/java/com/aijiuyi/admin/entity/DeviceGroup.java
  20. 50 0
      code/backend/src/main/java/com/aijiuyi/admin/entity/DeviceGroupMember.java
  21. 12 0
      code/backend/src/main/java/com/aijiuyi/admin/mapper/DeviceGroupMapper.java
  22. 12 0
      code/backend/src/main/java/com/aijiuyi/admin/mapper/DeviceGroupMemberMapper.java
  23. 31 0
      code/backend/src/main/java/com/aijiuyi/admin/service/AppDeviceGroupService.java
  24. 962 0
      code/backend/src/main/java/com/aijiuyi/admin/service/impl/AppDeviceGroupServiceImpl.java
  25. 12 0
      code/backend/src/main/java/com/aijiuyi/admin/service/impl/DeviceServiceImpl.java
  26. 4 0
      code/backend/src/main/resources/mapper/DeviceGroupMapper.xml
  27. 4 0
      code/backend/src/main/resources/mapper/DeviceGroupMemberMapper.xml
  28. 9 0
      code/backend/src/main/resources/mapper/DeviceMapper.xml
  29. 16 0
      code/backend/src/main/resources/rebel.xml
  30. 31 0
      code/backend/src/main/resources/sql/migration_device_group.sql
  31. 46 0
      code/frontend/src/views/device/index.vue

+ 105 - 34
code/ajyApp/pages/device/bodyParams/bodyParams.nvue

@@ -20,6 +20,13 @@
 					</view>
 				</view>
 
+				<view class="formItem">
+					<text class="formLabel">手机号(选填)</text>
+					<view class="inputWrap">
+						<input class="formInput" v-model="form.phone" type="number" placeholder="填写后可作为登录账号" placeholder-class="pholder" maxlength="11" />
+					</view>
+				</view>
+
 				<!-- 性别 -->
 				<view class="formItem">
 					<text class="formLabel">性别</text>
@@ -42,17 +49,17 @@
 					</view>
 				</view>
 
-				<!-- 高 -->
+				<!-- 身长/坐高 -->
 				<view class="formItem">
-					<text class="formLabel">肩高(地面到肩膀的高度)</text>
+					<text class="formLabel">身长/坐高</text>
 					<view class="inputWrap">
-						<input class="formInput" v-model="form.shoulderHeight" type="digit" placeholder="例如:135" placeholder-class="pholder" />
+						<input class="formInput" v-model="form.bodyHeight" type="digit" placeholder="例如:135" placeholder-class="pholder" />
 						<text class="unitTxt">cm</text>
 					</view>
 					<view class="sliderWrap">
-						<slider :value="Number(form.shoulderHeight) || 100" :min="50" :max="250" :step="1" 
+						<slider :value="Number(form.bodyHeight) || 100" :min="50" :max="250" :step="1"
 							activeColor="#389588" backgroundColor="#E8E8E8" block-size="20"
-							@change="onShoulderHeightChange" />
+							@change="onBodyHeightChange" />
 						<view class="sliderRange">
 							<text class="rangeTxt">50cm</text>
 							<text class="rangeTxt">250cm</text>
@@ -60,6 +67,24 @@
 					</view>
 				</view>
 
+				<!-- 脊柱长度 -->
+				<view class="formItem">
+					<text class="formLabel">C7-S4脊柱长度</text>
+					<view class="inputWrap">
+						<input class="formInput" v-model="form.spineLength" type="digit" placeholder="例如:45" placeholder-class="pholder" />
+						<text class="unitTxt">cm</text>
+					</view>
+					<view class="sliderWrap">
+						<slider :value="Number(form.spineLength) || 45" :min="1" :max="80" :step="1"
+							activeColor="#389588" backgroundColor="#E8E8E8" block-size="20"
+							@change="onSpineLengthChange" />
+						<view class="sliderRange">
+							<text class="rangeTxt">1cm</text>
+							<text class="rangeTxt">80cm</text>
+						</view>
+					</view>
+				</view>
+
 				<!-- 肩宽 -->
 				<view class="formItem">
 					<text class="formLabel">肩宽(两肩之间的宽度)</text>
@@ -101,6 +126,8 @@
 
 <script>
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
+import { addGroupMember, updateGroupMember } from '@/utils/api/device'
+import { updateProfile } from '@/utils/api/user'
 
 export default {
 	components: {
@@ -113,10 +140,12 @@ export default {
 			userId: '',
 			form: {
 				name: '',
+				phone: '',
 				gender: 1,
 				age: '',
-				shoulderHeight: '',
+				bodyHeight: '',
 				shoulderWidth: '',
+				spineLength: '',
 				weight: ''
 			}
 		}
@@ -137,32 +166,49 @@ export default {
 		/** 加载编辑数据 */
 		loadUserData() {
 			const list = uni.getStorageSync('user_manage_list') || []
-			const user = list.find(u => u.id === this.userId)
+			const currentUser = uni.getStorageSync('current_manage_user') || {}
+			const user = list.find(u => String(u.id) === String(this.userId)) ||
+				((String(currentUser.id) === String(this.userId) || String(currentUser.profileId) === String(this.userId)) ? currentUser : null)
 			if (user) {
 				this.form.name = user.name || ''
+				this.form.phone = user.phone || ''
 				this.form.gender = user.gender || 1
 				this.form.age = user.age ? String(user.age) : ''
-				this.form.shoulderHeight = user.shoulderHeight ? String(user.shoulderHeight) : ''
+				this.form.bodyHeight = user.bodyHeight || user.shoulderHeight ? String(user.bodyHeight || user.shoulderHeight) : ''
 				this.form.shoulderWidth = user.shoulderWidth ? String(user.shoulderWidth) : ''
+				this.form.spineLength = user.spineLength ? String(user.spineLength) : ''
 				this.form.weight = user.weight ? String(user.weight) : ''
 			}
 		},
-		onShoulderHeightChange(e) {
-			this.form.shoulderHeight = String(e.detail.value)
+		onBodyHeightChange(e) {
+			this.form.bodyHeight = String(e.detail.value)
 		},
 		onShoulderWidthChange(e) {
 			this.form.shoulderWidth = String(e.detail.value)
 		},
+		onSpineLengthChange(e) {
+			this.form.spineLength = String(e.detail.value)
+		},
 		/** 校验并保存 */
-		onSave() {
+		async onSave() {
 			// 校验
 			if (!this.form.name.trim()) {
 				this.$refs.ayToast.error('请输入姓名')
 				return
 			}
-			const height = Number(this.form.shoulderHeight)
+			const phone = this.form.phone.trim()
+			if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
+				this.$refs.ayToast.error('手机号格式不正确')
+				return
+			}
+			const age = Number(this.form.age)
+			if (!age || age < 1 || age > 120) {
+				this.$refs.ayToast.error('年龄范围为1-120岁')
+				return
+			}
+			const height = Number(this.form.bodyHeight)
 			if (!height || height < 50 || height > 250) {
-				this.$refs.ayToast.error('肩高范围为50-250cm')
+				this.$refs.ayToast.error('身长/坐高范围为50-250cm')
 				return
 			}
 			const width = Number(this.form.shoulderWidth)
@@ -170,13 +216,29 @@ export default {
 				this.$refs.ayToast.error('肩宽范围为20-80cm')
 				return
 			}
-			const weight = Number(this.form.weight)
-			if (!weight || weight < 10 || weight > 300) {
+			const spineLength = Number(this.form.spineLength)
+			if (!spineLength || spineLength < 1 || spineLength > 80) {
+				this.$refs.ayToast.error('脊柱长度范围为1-80cm')
+				return
+			}
+			const weight = this.form.weight ? Number(this.form.weight) : null
+			if (weight && (weight < 10 || weight > 300)) {
 				this.$refs.ayToast.error('体重范围为10-300kg')
 				return
 			}
 
 			let list = uni.getStorageSync('user_manage_list') || []
+			const currentDevice = uni.getStorageSync('current_device') || {}
+			const payload = {
+				name: this.form.name.trim(),
+				phone,
+				gender: this.form.gender,
+				age,
+				bodyHeight: height,
+				shoulderWidth: width,
+				spineLength,
+				weight
+			}
 
 			if (this.mode === 'add') {
 				// 再次检查是否超过10个
@@ -184,16 +246,12 @@ export default {
 					this.$refs.ayToast.error('最多可添加10个用户')
 					return
 				}
-				const newUser = {
-					id: 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6),
-					name: this.form.name.trim(),
-					gender: this.form.gender,
-					age: Number(this.form.age) || 0,
-					shoulderHeight: height,
-					shoulderWidth: width,
-					weight: weight,
-					createTime: Date.now()
+				if (!currentDevice.groupId) {
+					this.$refs.ayToast.error('公共设备不支持新增成员')
+					return
 				}
+				const saved = await addGroupMember(currentDevice.groupId, payload)
+				const newUser = this.normalizeSavedMember(saved)
 				list.push(newUser)
 				uni.setStorageSync('user_manage_list', list)
 				// 如果是第一个用户,自动设为当前用户
@@ -204,24 +262,29 @@ export default {
 				this.$refs.ayToast.success('添加成功')
 			} else {
 				// 编辑模式
-				const index = list.findIndex(u => u.id === this.userId)
+				let index = list.findIndex(u => String(u.id) === String(this.userId))
+				if (index === -1) {
+					const currentUser = uni.getStorageSync('current_manage_user') || {}
+					if (String(currentUser.id) === String(this.userId) || String(currentUser.profileId) === String(this.userId)) {
+						list = [this.normalizeSavedMember(currentUser)]
+						index = 0
+					}
+				}
 				if (index === -1) {
 					this.$refs.ayToast.error('用户信息已过期,请刷新')
 					return
 				}
-				list[index] = {
-					...list[index],
-					name: this.form.name.trim(),
-					gender: this.form.gender,
-					age: Number(this.form.age) || 0,
-					shoulderHeight: height,
-					shoulderWidth: width,
-					weight: weight
+				let saved
+				if (currentDevice.groupId) {
+					saved = await updateGroupMember(currentDevice.groupId, this.userId, payload)
+				} else {
+					saved = await updateProfile(payload)
 				}
+				list[index] = this.normalizeSavedMember({ ...list[index], ...(saved || payload) })
 				uni.setStorageSync('user_manage_list', list)
 				// 如果编辑的是当前用户,更新当前用户缓存
 				const curId = uni.getStorageSync('current_manage_user_id')
-				if (curId === this.userId) {
+				if (String(curId) === String(this.userId)) {
 					uni.setStorageSync('current_manage_user', list[index])
 				}
 				this.$refs.ayToast.success('保存成功')
@@ -230,6 +293,14 @@ export default {
 			setTimeout(() => {
 				uni.navigateBack()
 			}, 800)
+		},
+		normalizeSavedMember(item) {
+			return {
+				...item,
+				id: String(item.profileId || item.id),
+				bodyHeight: item.bodyHeight || item.shoulderHeight || '',
+				profileComplete: !!item.profileComplete
+			}
 		}
 	}
 }

+ 14 - 0
code/ajyApp/pages/device/deviceInfo/deviceInfo.nvue

@@ -335,6 +335,7 @@ export default {
 		return {
 			statusBarHeight: 44,
 			deviceId: '',
+			deviceCode: '',
 			deviceName: '',
 			rssi: '',
 			subTime: '00:00:00',
@@ -355,8 +356,16 @@ export default {
 		this.statusBarHeight = sysInfo.statusBarHeight || 44
 
 		this.deviceId = options.deviceId || ''
+		this.deviceCode = options.deviceCode || ''
 		this.deviceName = options.name || ''
 		this.rssi = options.rssi || ''
+		const currentDevice = uni.getStorageSync('current_device') || {}
+		if (currentDevice.deviceCode && !this.deviceCode) {
+			this.deviceCode = currentDevice.deviceCode
+		}
+		if (currentDevice.name && !this.deviceName) {
+			this.deviceName = currentDevice.name
+		}
 
 		// TODO: 后续接口确定后实现 - 下载穴位列表
 		// TODO: 后续接口确定后实现 - 下载专业模式推荐方案
@@ -384,9 +393,14 @@ export default {
 		},
 		/** 加载当前用户信息 */
 		loadCurrentUser() {
+			const currentDevice = uni.getStorageSync('current_device') || {}
 			const user = uni.getStorageSync('current_manage_user')
 			if (user && user.name) {
 				this.currentUserName = user.name
+			} else if (currentDevice.profile && currentDevice.profile.name) {
+				uni.setStorageSync('current_manage_user', currentDevice.profile)
+				uni.setStorageSync('current_manage_user_id', currentDevice.profile.profileId || currentDevice.profile.id)
+				this.currentUserName = currentDevice.profile.name
 			} else {
 				this.currentUserName = ''
 			}

+ 70 - 1
code/ajyApp/pages/device/deviceInfo/mixins/ble-mixin.js

@@ -4,6 +4,7 @@
  */
 import bleManager, { BLE_STATE, MODE, MOXI_STATE, POWER, MUTE, TEMPERATURE, CHAIR_ANGLE } from '@/utils/ble'
 import { ensureBlePrerequisite } from '@/utils/ble/permission.js'
+import { selectDevice } from '@/utils/api/device'
 
 export default {
 	data() {
@@ -179,7 +180,10 @@ export default {
 		},
 
 		// ========== 开始艾灸 ==========
-		startDeviceEvt() {
+		async startDeviceEvt() {
+			const ready = await this.ensureMoxibustionReady()
+			if (!ready) return
+
 			this.deviceStatus = 1
 			this.ispreHot = false
 			let totalDuration = 0
@@ -211,6 +215,71 @@ export default {
 			}
 		},
 
+		async ensureMoxibustionReady() {
+			const token = uni.getStorageSync('user_token')
+			if (!token) {
+				this.$refs.ayToast.error('请先登录')
+				uni.navigateTo({ url: '/pages/login/login' })
+				return false
+			}
+
+			const currentDevice = uni.getStorageSync('current_device') || {}
+			const currentUser = uni.getStorageSync('current_manage_user') || {}
+			const deviceCode = currentDevice.deviceCode || this.deviceCode || this.deviceName || this.deviceId
+			if (!deviceCode) {
+				this.$refs.ayToast.error('请先选择设备')
+				return false
+			}
+
+			const profileId = currentUser.profileId || currentUser.id || currentDevice.profileId
+			if (!profileId) {
+				this.$refs.ayToast.error('请先选择用户')
+				uni.navigateTo({ url: '/pages/device/userManage/userManage' })
+				return false
+			}
+
+			let selected
+			try {
+				selected = await selectDevice({
+					deviceCode,
+					groupId: currentDevice.groupId || null,
+					profileId
+				})
+			} catch (e) {
+				console.error('设备使用权限校验失败', e)
+				return false
+			}
+
+			const profile = selected.profile || currentUser
+			uni.setStorageSync('current_device', { ...currentDevice, ...selected })
+			uni.setStorageSync('current_manage_user', profile)
+			uni.setStorageSync('current_manage_user_id', profile.profileId || profile.id)
+
+			if (!this.isProfileCompleteForMoxibustion(profile)) {
+				this.$refs.ayToast.error('请先补全用户资料')
+				setTimeout(() => {
+					uni.navigateTo({
+						url: '/pages/device/bodyParams/bodyParams?mode=edit&userId=' + encodeURIComponent(profile.profileId || profile.id || '')
+					})
+				}, 600)
+				return false
+			}
+			return true
+		},
+
+		isProfileCompleteForMoxibustion(profile) {
+			if (!profile) return false
+			const bodyHeight = profile.bodyHeight || profile.shoulderHeight
+			return !!(
+				profile.name &&
+				profile.gender &&
+				profile.age &&
+				profile.shoulderWidth &&
+				bodyHeight &&
+				profile.spineLength
+			)
+		},
+
 		// ========== 暂停/继续 ==========
 		stopAijiu() {
 			if (this.deviceStatus == 5) {

+ 42 - 4
code/ajyApp/pages/device/list/list.nvue

@@ -32,6 +32,7 @@
 
 <script>
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
+import { getDeviceList, selectDevice } from '@/utils/api/device'
 
 export default {
 	components: {
@@ -53,8 +54,21 @@ export default {
 		this.init()
 	},
 	methods: {
-		init() {
-			// 从本地存储读取设备列表
+		async init() {
+			try {
+				const data = await getDeviceList()
+				this.list = (data || []).map(item => ({
+					...item,
+					deviceId: item.deviceCode || item.deviceId,
+					name: item.deviceName || item.name || item.deviceCode,
+					RSSI: item.RSSI || ''
+				}))
+				uni.setStorageSync('deviceList', this.list)
+				return
+			} catch (e) {
+				console.error('加载设备列表失败,使用本地缓存', e)
+			}
+
 			try {
 				const data = uni.getStorageSync('deviceList')
 				if (data && data.length > 0) {
@@ -71,9 +85,33 @@ export default {
 				url: '/pages/device/search/search'
 			})
 		},
-		goDeviceInfo(item) {
+		async goDeviceInfo(item) {
+			try {
+				const selected = await selectDevice({
+					deviceCode: item.deviceCode || item.deviceId,
+					groupId: item.groupId || null,
+					profileId: item.profileId || null
+				})
+				const merged = {
+					...item,
+					...selected,
+					deviceId: item.deviceId || selected.deviceCode,
+					name: selected.deviceName || item.name
+				}
+				uni.setStorageSync('current_device', merged)
+				if (selected.profile) {
+					uni.setStorageSync('current_manage_user_id', selected.profile.profileId || selected.profile.id)
+					uni.setStorageSync('current_manage_user', selected.profile)
+				}
+			} catch (e) {
+				console.error('选择设备失败', e)
+				return
+			}
 			uni.navigateTo({
-				url: '/pages/device/deviceInfo/deviceInfo?name=' + encodeURIComponent(item.name || '') + '&deviceId=' + encodeURIComponent(item.deviceId || '') + '&rssi=' + encodeURIComponent(item.RSSI || '')
+				url: '/pages/device/deviceInfo/deviceInfo?name=' + encodeURIComponent(item.name || '') +
+					'&deviceId=' + encodeURIComponent(item.deviceId || '') +
+					'&deviceCode=' + encodeURIComponent(item.deviceCode || item.deviceId || '') +
+					'&rssi=' + encodeURIComponent(item.RSSI || '')
 			})
 		}
 	}

+ 34 - 7
code/ajyApp/pages/device/search/search.nvue

@@ -50,6 +50,7 @@ import bleManager, {
 	BLE_STATE, ensureBlePrerequisite, openLocationSettings
 } from '@/utils/ble'
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
+import { bindDevice } from '@/utils/api/device'
 
 export default {
 	components: {
@@ -158,8 +159,20 @@ export default {
 			if (val >= -95) return 1   // 弱
 			return 1
 		},
-		onSelectDevice(device) {
-			// 将设备添加到本地存储的设备列表
+		async onSelectDevice(device) {
+			const deviceCode = device.name || device.localName || device.deviceId
+			if (!deviceCode) {
+				this.$refs.ayToast.error('未识别到设备编号')
+				return
+			}
+			let bindResult
+			try {
+				bindResult = await bindDevice({ deviceCode })
+			} catch (e) {
+				console.error('绑定设备失败', e)
+				return
+			}
+
 			let deviceList = []
 			try {
 				const data = uni.getStorageSync('deviceList')
@@ -169,19 +182,33 @@ export default {
 			} catch (e) {}
 
 			// 检查是否已添加
-			const exists = deviceList.find(item => item.deviceId === device.deviceId)
+			const exists = deviceList.find(item =>
+				item.deviceId === device.deviceId || item.deviceCode === bindResult.deviceCode
+			)
 			if (exists) {
 				this.$refs.ayToast.show('该设备已添加')
 				return
 			}
 
-			// 添加设备
-			deviceList.push({
+			const newDevice = {
 				deviceId: device.deviceId,
-				name: device.name || '艾灸椅',
+				bleDeviceName: device.name || '',
+				name: bindResult.deviceName || device.name || '艾灸椅',
+				deviceCode: bindResult.deviceCode || deviceCode,
+				deviceType: bindResult.deviceType || 1,
+				groupId: bindResult.groupId || null,
+				profileId: bindResult.profileId || null,
+				profileComplete: !!bindResult.profileComplete,
+				profile: bindResult.profile || null,
 				RSSI: device.RSSI
-			})
+			}
+			deviceList.push(newDevice)
 			uni.setStorageSync('deviceList', deviceList)
+			uni.setStorageSync('current_device', newDevice)
+			if (bindResult.profile) {
+				uni.setStorageSync('current_manage_user_id', bindResult.profile.profileId || bindResult.profile.id)
+				uni.setStorageSync('current_manage_user', bindResult.profile)
+			}
 			this.$refs.ayToast.success('设备添加成功')
 
 			// 延迟返回

+ 35 - 15
code/ajyApp/pages/device/userManage/userManage.nvue

@@ -29,9 +29,9 @@
 									<text class="userAge" v-if="item.age">{{item.age}}岁</text>
 								</view>
 								<view class="bodyRow">
-									<text class="bodyTxt">肩高:{{item.shoulderHeight || '--'}}cm</text>
+									<text class="bodyTxt">身长:{{item.bodyHeight || item.shoulderHeight || '--'}}cm</text>
 									<text class="bodyTxt">肩宽:{{item.shoulderWidth || '--'}}cm</text>
-									<text class="bodyTxt">体重:{{item.weight || '--'}}kg</text>
+									<text class="bodyTxt">脊柱:{{item.spineLength || '--'}}cm</text>
 								</view>
 							</view>
 						</view>
@@ -72,6 +72,7 @@
 <script>
 import customPopup from '@/components/customPopup/customPopup.nvue'
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
+import { getGroupMembers } from '@/utils/api/device'
 
 export default {
 	components: {
@@ -98,21 +99,45 @@ export default {
 		goBack() {
 			uni.navigateBack()
 		},
-		/** 从本地存储加载用户列表 */
-		loadUsers() {
-			const list = uni.getStorageSync('user_manage_list') || []
+		/** 从后端加载当前设备用户列表 */
+		async loadUsers() {
+			const currentDevice = uni.getStorageSync('current_device') || {}
+			let list = []
+			if (currentDevice.groupId) {
+				try {
+					const data = await getGroupMembers(currentDevice.groupId)
+					list = (data || []).map(this.normalizeMember)
+					uni.setStorageSync('user_manage_list', list)
+				} catch (e) {
+					console.error('加载群组成员失败,使用本地缓存', e)
+					list = uni.getStorageSync('user_manage_list') || []
+				}
+			} else {
+				const currentUser = uni.getStorageSync('current_manage_user') || currentDevice.profile
+				list = currentUser ? [this.normalizeMember(currentUser)] : []
+				uni.setStorageSync('user_manage_list', list)
+			}
 			this.userList = list
-			this.currentUserId = uni.getStorageSync('current_manage_user_id') || ''
+			this.currentUserId = String(uni.getStorageSync('current_manage_user_id') || '')
 			// 如果没有当前用户且列表不为空,默认选中第一个
 			if (!this.currentUserId && list.length > 0) {
-				this.currentUserId = list[0].id
+				this.currentUserId = String(list[0].id)
 				uni.setStorageSync('current_manage_user_id', this.currentUserId)
+				uni.setStorageSync('current_manage_user', list[0])
+			}
+		},
+		normalizeMember(item) {
+			return {
+				...item,
+				id: String(item.profileId || item.id),
+				bodyHeight: item.bodyHeight || item.shoulderHeight || '',
+				profileComplete: !!item.profileComplete
 			}
 		},
 		/** 切换用户 */
 		switchUser(item) {
-			this.currentUserId = item.id
-			uni.setStorageSync('current_manage_user_id', item.id)
+			this.currentUserId = String(item.id)
+			uni.setStorageSync('current_manage_user_id', this.currentUserId)
 			uni.setStorageSync('current_manage_user', item)
 			this.$refs.ayToast.success('已切换至用户:' + item.name)
 			setTimeout(() => {
@@ -137,12 +162,7 @@ export default {
 		},
 		/** 确认删除 */
 		confirmDelete(item) {
-			if (this.userList.length <= 1) {
-				this.$refs.ayToast.error('至少保留一个用户')
-				return
-			}
-			this.deleteTarget = item
-			this.showDeletePopup = true
+			this.$refs.ayToast.error('暂不支持在App端删除群组成员')
 		},
 		/** 录入指纹 */
 		enrollFingerprint(item) {

+ 18 - 2
code/ajyApp/pages/my/info/info.nvue

@@ -60,7 +60,7 @@
 
 <script>
 import ayToast from '@/components/ay-toast/ay-toast.nvue'
-import { uploadAvatar as uploadAvatarApi, updateProfile } from '@/utils/api/user.js'
+import { getProfile, uploadAvatar as uploadAvatarApi, updateProfile } from '@/utils/api/user.js'
 
 export default {
 	components: {
@@ -81,7 +81,23 @@ export default {
 		this.loadUserInfo()
 	},
 	methods: {
-		loadUserInfo() {
+		async loadUserInfo() {
+			try {
+				const profile = await getProfile()
+				this.phone = profile.accountPhone || profile.phone || ''
+				this.nickname = profile.nickname || '用户'
+				this.appid = profile.appId || ''
+				this.avatar = profile.avatar || '/static/144x144.png'
+				uni.setStorageSync('phone', this.phone)
+				uni.setStorageSync('nickname', this.nickname)
+				uni.setStorageSync('appId', this.appid)
+				uni.setStorageSync('avatar', this.avatar)
+				uni.setStorageSync('current_manage_user', profile)
+				uni.setStorageSync('current_manage_user_id', profile.profileId || profile.id)
+				return
+			} catch (e) {
+				console.error('加载个人资料失败,使用本地缓存', e)
+			}
 			const phone = uni.getStorageSync('phone') || '138****8888'
 			const nickname = uni.getStorageSync('nickname') || '用户'
 			const appid = uni.getStorageSync('appId') || ''

+ 3 - 3
code/ajyApp/utils/api/user.js

@@ -5,15 +5,15 @@ import http from '@/utils/http'
 
 /**
  * 获取当前用户个人资料
- * @returns {Promise<{userId, nickname, avatar, phone, appId}>}
+ * @returns {Promise<Object>} 当前账号与完整用户档案信息
  */
 export function getProfile() {
 	return http.get('/app/user/profile')
 }
 
 /**
- * 修改当前用户个人资料(昵称、头像)
- * @param {Object} data - { nickname?, avatar? }
+ * 修改当前用户个人资料
+ * @param {Object} data - 昵称、头像及用户档案字段
  */
 export function updateProfile(data) {
 	return http.put('/app/user/profile', data)

+ 48 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/AppDeviceController.java

@@ -0,0 +1,48 @@
+package com.aijiuyi.admin.controller;
+
+import com.aijiuyi.admin.common.annotation.Log;
+import com.aijiuyi.admin.common.context.RequestContext;
+import com.aijiuyi.admin.common.entity.Result;
+import com.aijiuyi.admin.common.enums.OperationType;
+import com.aijiuyi.admin.controller.dto.AppDeviceBindDTO;
+import com.aijiuyi.admin.controller.dto.AppDeviceSelectDTO;
+import com.aijiuyi.admin.service.AppDeviceGroupService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * App设备使用入口。
+ */
+@RestController
+@RequestMapping("/app/device")
+public class AppDeviceController {
+
+    @Autowired
+    private AppDeviceGroupService appDeviceGroupService;
+
+    @PostMapping("/bind")
+    @Log(value = "App绑定设备", module = "App设备", operationType = OperationType.INSERT)
+    public Result<Map<String, Object>> bind(@RequestBody @Valid AppDeviceBindDTO dto) {
+        return Result.success(appDeviceGroupService.bindDevice(RequestContext.getUserId(), dto));
+    }
+
+    @GetMapping("/list")
+    @Log(value = "App查询可用设备", module = "App设备", operationType = OperationType.QUERY)
+    public Result<List<Map<String, Object>>> list() {
+        return Result.success(appDeviceGroupService.listDevices(RequestContext.getUserId()));
+    }
+
+    @PostMapping("/select")
+    @Log(value = "App选择当前设备", module = "App设备", operationType = OperationType.UPDATE)
+    public Result<Map<String, Object>> select(@RequestBody @Valid AppDeviceSelectDTO dto) {
+        return Result.success(appDeviceGroupService.selectDevice(RequestContext.getUserId(), dto));
+    }
+}

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

@@ -0,0 +1,52 @@
+package com.aijiuyi.admin.controller;
+
+import com.aijiuyi.admin.common.annotation.Log;
+import com.aijiuyi.admin.common.context.RequestContext;
+import com.aijiuyi.admin.common.entity.Result;
+import com.aijiuyi.admin.common.enums.OperationType;
+import com.aijiuyi.admin.controller.dto.AppGroupMemberDTO;
+import com.aijiuyi.admin.service.AppDeviceGroupService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * App家庭群组成员管理。
+ */
+@RestController
+@RequestMapping("/app/group")
+public class AppGroupController {
+
+    @Autowired
+    private AppDeviceGroupService appDeviceGroupService;
+
+    @GetMapping("/{groupId}/members")
+    @Log(value = "App查询群组成员", module = "App群组", operationType = OperationType.QUERY)
+    public Result<List<Map<String, Object>>> members(@PathVariable Long groupId) {
+        return Result.success(appDeviceGroupService.listGroupMembers(RequestContext.getUserId(), groupId));
+    }
+
+    @PostMapping("/{groupId}/members")
+    @Log(value = "App新增群组成员", module = "App群组", operationType = OperationType.INSERT)
+    public Result<Map<String, Object>> addMember(@PathVariable Long groupId,
+                                                 @RequestBody @Valid AppGroupMemberDTO dto) {
+        return Result.success(appDeviceGroupService.addGroupMember(RequestContext.getUserId(), groupId, dto));
+    }
+
+    @PutMapping("/{groupId}/members/{profileId}")
+    @Log(value = "App修改群组成员", module = "App群组", operationType = OperationType.UPDATE)
+    public Result<Map<String, Object>> updateMember(@PathVariable Long groupId,
+                                                    @PathVariable Long profileId,
+                                                    @RequestBody @Valid AppGroupMemberDTO dto) {
+        return Result.success(appDeviceGroupService.updateGroupMember(RequestContext.getUserId(), groupId, profileId, dto));
+    }
+}

+ 13 - 39
code/backend/src/main/java/com/aijiuyi/admin/controller/AppProfileController.java

@@ -4,63 +4,37 @@ import com.aijiuyi.admin.common.annotation.Log;
 import com.aijiuyi.admin.common.context.RequestContext;
 import com.aijiuyi.admin.common.entity.Result;
 import com.aijiuyi.admin.common.enums.OperationType;
-import com.aijiuyi.admin.entity.AppUser;
-import com.aijiuyi.admin.service.AppUserService;
+import com.aijiuyi.admin.controller.dto.AppProfileDTO;
+import com.aijiuyi.admin.service.AppDeviceGroupService;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
-import java.util.HashMap;
+import javax.validation.Valid;
 import java.util.Map;
 
 /**
- * App 端用户个人资料 Controller
- * 已登录用户获取/修改自己的个人信息
+ * App端用户个人资料 Controller。
  */
 @RestController
 @RequestMapping("/app/user")
 public class AppProfileController {
 
     @Autowired
-    private AppUserService appUserService;
+    private AppDeviceGroupService appDeviceGroupService;
 
-    /**
-     * 获取当前用户个人信息
-     *
-     * @return 用户基本信息(nickname、avatar、phone、appId)
-     */
     @GetMapping("/profile")
     @Log(value = "获取个人资料", module = "App用户", operationType = OperationType.QUERY)
     public Result<Map<String, Object>> getProfile() {
-        Long userId = RequestContext.getUserId();
-        AppUser user = appUserService.getById(userId);
-        Map<String, Object> data = new HashMap<>(8);
-        data.put("userId", user.getId());
-        data.put("nickname", user.getNickname());
-        data.put("avatar", user.getAvatar());
-        data.put("phone", user.getPhone());
-        data.put("appId", user.getAppId());
-        return Result.success(data);
+        return Result.success(appDeviceGroupService.getCurrentProfile(RequestContext.getUserId()));
     }
 
-    /**
-     * 修改当前用户个人信息(昵称、头像)
-     *
-     * @param body 包含 nickname 和/或 avatar 的请求体
-     * @return 操作结果
-     */
     @PutMapping("/profile")
     @Log(value = "修改个人资料", module = "App用户", operationType = OperationType.UPDATE)
-    public Result<Void> updateProfile(@RequestBody Map<String, String> body) {
-        Long userId = RequestContext.getUserId();
-        AppUser update = new AppUser();
-        update.setId(userId);
-        if (body.containsKey("nickname")) {
-            update.setNickname(body.get("nickname"));
-        }
-        if (body.containsKey("avatar")) {
-            update.setAvatar(body.get("avatar"));
-        }
-        appUserService.updateById(update);
-        return Result.success();
+    public Result<Map<String, Object>> updateProfile(@RequestBody @Valid AppProfileDTO dto) {
+        return Result.success(appDeviceGroupService.updateCurrentProfile(RequestContext.getUserId(), dto));
     }
 }

+ 16 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppDeviceBindDTO.java

@@ -0,0 +1,16 @@
+package com.aijiuyi.admin.controller.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * App设备绑定请求。
+ */
+@Data
+public class AppDeviceBindDTO {
+
+    /** 设备编号 */
+    @NotBlank(message = "设备编号不能为空")
+    private String deviceCode;
+}

+ 22 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppDeviceSelectDTO.java

@@ -0,0 +1,22 @@
+package com.aijiuyi.admin.controller.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * App当前使用设备选择请求。
+ */
+@Data
+public class AppDeviceSelectDTO {
+
+    /** 设备编号 */
+    @NotBlank(message = "设备编号不能为空")
+    private String deviceCode;
+
+    /** 家庭设备群组ID,公共设备可为空 */
+    private Long groupId;
+
+    /** 当前选择的用户档案ID */
+    private Long profileId;
+}

+ 62 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppGroupMemberDTO.java

@@ -0,0 +1,62 @@
+package com.aijiuyi.admin.controller.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+import java.math.BigDecimal;
+
+/**
+ * App家庭群组成员资料请求。
+ */
+@Data
+public class AppGroupMemberDTO {
+
+    /** 姓名 */
+    @NotBlank(message = "姓名不能为空")
+    private String name;
+
+    /** 性别:1=男,2=女 */
+    private Integer gender;
+
+    /** 年龄 */
+    private Integer age;
+
+    /** 手机号,App端可为空 */
+    @Pattern(regexp = "^$|^1[3-9]\\d{9}$", message = "手机号格式不正确")
+    private String phone;
+
+    /** 用户分类ID */
+    private Long userCategoryId;
+
+    private String provinceCode;
+
+    private String cityCode;
+
+    private String districtCode;
+
+    private String address;
+
+    /** 肩宽(cm) */
+    private BigDecimal shoulderWidth;
+
+    /** 身长/坐高(cm) */
+    private BigDecimal bodyHeight;
+
+    /** C7-S4脊柱长度(cm) */
+    private BigDecimal spineLength;
+
+    /** 身高(cm) */
+    private BigDecimal height;
+
+    /** 体重(kg) */
+    private BigDecimal weight;
+
+    private BigDecimal fingerWidth1;
+
+    private BigDecimal fingerWidth15;
+
+    private BigDecimal fingerWidth3;
+
+    private Long acupointTableId;
+}

+ 52 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/AppProfileDTO.java

@@ -0,0 +1,52 @@
+package com.aijiuyi.admin.controller.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.Pattern;
+import java.math.BigDecimal;
+
+/**
+ * App当前账号个人档案请求。
+ */
+@Data
+public class AppProfileDTO {
+
+    private String nickname;
+
+    private String avatar;
+
+    private String name;
+
+    private Integer gender;
+
+    private Integer age;
+
+    @Pattern(regexp = "^$|^1[3-9]\\d{9}$", message = "手机号格式不正确")
+    private String phone;
+
+    private String provinceCode;
+
+    private String cityCode;
+
+    private String districtCode;
+
+    private String address;
+
+    private BigDecimal shoulderWidth;
+
+    private BigDecimal bodyHeight;
+
+    private BigDecimal spineLength;
+
+    private BigDecimal height;
+
+    private BigDecimal weight;
+
+    private BigDecimal fingerWidth1;
+
+    private BigDecimal fingerWidth15;
+
+    private BigDecimal fingerWidth3;
+
+    private Long acupointTableId;
+}

+ 3 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/DeviceQueryDTO.java

@@ -17,6 +17,9 @@ public class DeviceQueryDTO {
     /** 绑定用户姓名(模糊搜索,匹配该设备下任意绑定用户) */
     private String boundUserName;
 
+    /** 设备类型:1=家庭设备,2=公共设备 */
+    private Integer deviceType;
+
     /** 在线状态:1=在线,0=离线,null=全部 */
     private Integer onlineStatus;
 

+ 3 - 0
code/backend/src/main/java/com/aijiuyi/admin/controller/dto/DeviceSaveDTO.java

@@ -22,6 +22,9 @@ public class DeviceSaveDTO {
     /** 设备型号(必填,AJY-2026 / AJY-2025) */
     private String deviceModel;
 
+    /** 设备类型:1=家庭设备,2=公共设备 */
+    private Integer deviceType;
+
     /** 设备序列号(唯一,必填,最长50字符) */
     private String serialNo;
 

+ 3 - 0
code/backend/src/main/java/com/aijiuyi/admin/entity/Device.java

@@ -27,6 +27,9 @@ public class Device {
     /** 设备型号(AJY-2026 / AJY-2025) */
     private String deviceModel;
 
+    /** 设备类型:1=家庭设备,2=公共设备 */
+    private Integer deviceType;
+
     /** 设备序列号(唯一) */
     private String serialNo;
 

+ 41 - 0
code/backend/src/main/java/com/aijiuyi/admin/entity/DeviceGroup.java

@@ -0,0 +1,41 @@
+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.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 家庭设备群组实体。
+ */
+@Data
+@TableName("device_group")
+public class DeviceGroup {
+
+    /** 主键ID */
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /** 家庭设备编号 */
+    private String deviceCode;
+
+    /** 状态:1=正常,0=停用 */
+    private Integer status;
+
+    /** 逻辑删除:0=未删除,1=已删除 */
+    @TableLogic
+    private Integer deleted;
+
+    /** 创建时间 */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /** 更新时间 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}

+ 50 - 0
code/backend/src/main/java/com/aijiuyi/admin/entity/DeviceGroupMember.java

@@ -0,0 +1,50 @@
+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.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 家庭设备群组成员实体。
+ */
+@Data
+@TableName("device_group_member")
+public class DeviceGroupMember {
+
+    /** 主键ID */
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /** 群组ID */
+    private Long groupId;
+
+    /** 用户档案ID */
+    private Long profileId;
+
+    /** 关联App账号ID,可为空 */
+    private Long appUserId;
+
+    /** 状态:1=正常,0=移除 */
+    private Integer status;
+
+    /** 加入时间 */
+    private LocalDateTime joinTime;
+
+    /** 逻辑删除:0=未删除,1=已删除 */
+    @TableLogic
+    private Integer deleted;
+
+    /** 创建时间 */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /** 更新时间 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}

+ 12 - 0
code/backend/src/main/java/com/aijiuyi/admin/mapper/DeviceGroupMapper.java

@@ -0,0 +1,12 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.DeviceGroup;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 家庭设备群组 Mapper。
+ */
+@Mapper
+public interface DeviceGroupMapper extends BaseMapper<DeviceGroup> {
+}

+ 12 - 0
code/backend/src/main/java/com/aijiuyi/admin/mapper/DeviceGroupMemberMapper.java

@@ -0,0 +1,12 @@
+package com.aijiuyi.admin.mapper;
+
+import com.aijiuyi.admin.entity.DeviceGroupMember;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 家庭设备群组成员 Mapper。
+ */
+@Mapper
+public interface DeviceGroupMemberMapper extends BaseMapper<DeviceGroupMember> {
+}

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

@@ -0,0 +1,31 @@
+package com.aijiuyi.admin.service;
+
+import com.aijiuyi.admin.controller.dto.AppDeviceBindDTO;
+import com.aijiuyi.admin.controller.dto.AppDeviceSelectDTO;
+import com.aijiuyi.admin.controller.dto.AppGroupMemberDTO;
+import com.aijiuyi.admin.controller.dto.AppProfileDTO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * App设备、家庭群组与用户档案服务。
+ */
+public interface AppDeviceGroupService {
+
+    Map<String, Object> bindDevice(Long appUserId, AppDeviceBindDTO dto);
+
+    List<Map<String, Object>> listDevices(Long appUserId);
+
+    Map<String, Object> selectDevice(Long appUserId, AppDeviceSelectDTO dto);
+
+    List<Map<String, Object>> listGroupMembers(Long appUserId, Long groupId);
+
+    Map<String, Object> addGroupMember(Long appUserId, Long groupId, AppGroupMemberDTO dto);
+
+    Map<String, Object> updateGroupMember(Long appUserId, Long groupId, Long profileId, AppGroupMemberDTO dto);
+
+    Map<String, Object> getCurrentProfile(Long appUserId);
+
+    Map<String, Object> updateCurrentProfile(Long appUserId, AppProfileDTO dto);
+}

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

@@ -0,0 +1,962 @@
+package com.aijiuyi.admin.service.impl;
+
+import com.aijiuyi.admin.common.constant.ResultCode;
+import com.aijiuyi.admin.common.exception.BusinessException;
+import com.aijiuyi.admin.controller.dto.AppDeviceBindDTO;
+import com.aijiuyi.admin.controller.dto.AppDeviceSelectDTO;
+import com.aijiuyi.admin.controller.dto.AppGroupMemberDTO;
+import com.aijiuyi.admin.controller.dto.AppProfileDTO;
+import com.aijiuyi.admin.entity.AppUser;
+import com.aijiuyi.admin.entity.Device;
+import com.aijiuyi.admin.entity.DeviceGroup;
+import com.aijiuyi.admin.entity.DeviceGroupMember;
+import com.aijiuyi.admin.entity.UserCategory;
+import com.aijiuyi.admin.entity.UserDevice;
+import com.aijiuyi.admin.entity.UserProfile;
+import com.aijiuyi.admin.mapper.AppUserMapper;
+import com.aijiuyi.admin.mapper.DeviceGroupMapper;
+import com.aijiuyi.admin.mapper.DeviceGroupMemberMapper;
+import com.aijiuyi.admin.mapper.DeviceMapper;
+import com.aijiuyi.admin.mapper.UserCategoryMapper;
+import com.aijiuyi.admin.mapper.UserDeviceMapper;
+import com.aijiuyi.admin.mapper.UserProfileMapper;
+import com.aijiuyi.admin.service.AppDeviceGroupService;
+import com.aijiuyi.admin.service.UserAcupointService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * App设备、家庭群组与用户档案服务实现。
+ */
+@Service
+public class AppDeviceGroupServiceImpl implements AppDeviceGroupService {
+
+    private static final int DEVICE_TYPE_FAMILY = 1;
+    private static final int DEVICE_TYPE_PUBLIC = 2;
+    private static final int NORMAL_USER_TYPE = 1;
+    private static final int STATUS_ENABLED = 1;
+    private static final String PHONE_PATTERN = "^1[3-9]\\d{9}$";
+
+    @Autowired
+    private AppUserMapper appUserMapper;
+
+    @Autowired
+    private DeviceMapper deviceMapper;
+
+    @Autowired
+    private DeviceGroupMapper deviceGroupMapper;
+
+    @Autowired
+    private DeviceGroupMemberMapper deviceGroupMemberMapper;
+
+    @Autowired
+    private UserProfileMapper userProfileMapper;
+
+    @Autowired
+    private UserDeviceMapper userDeviceMapper;
+
+    @Autowired
+    private UserCategoryMapper userCategoryMapper;
+
+    @Autowired
+    private UserAcupointService userAcupointService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> bindDevice(Long appUserId, AppDeviceBindDTO dto) {
+        AppUser appUser = requireAppUser(appUserId);
+        Device device = requireDevice(dto.getDeviceCode());
+        UserProfile accountProfile = ensureAccountProfile(appUser);
+
+        if (isPublicDevice(device)) {
+            ensureUserDevice(accountProfile, device);
+            return buildDeviceContext(device, null, null, accountProfile);
+        }
+
+        DeviceGroup group = ensureDeviceGroup(device);
+        DeviceGroupMember member = findActiveMemberByAppUserId(group.getId(), appUserId);
+        UserProfile profile;
+        if (member != null) {
+            profile = requireProfile(member.getProfileId());
+        } else {
+            member = findActiveMemberByProfileId(group.getId(), accountProfile.getId());
+            if (member == null) {
+                member = createGroupMember(group.getId(), accountProfile.getId(), appUserId);
+            } else if (!Objects.equals(member.getAppUserId(), appUserId)) {
+                DeviceGroupMember update = new DeviceGroupMember();
+                update.setId(member.getId());
+                update.setAppUserId(appUserId);
+                deviceGroupMemberMapper.updateById(update);
+                member.setAppUserId(appUserId);
+            }
+            profile = accountProfile;
+        }
+
+        ensureUserDevice(profile, device);
+        return buildDeviceContext(device, group, member, profile);
+    }
+
+    @Override
+    public List<Map<String, Object>> listDevices(Long appUserId) {
+        requireAppUser(appUserId);
+        Map<String, Map<String, Object>> result = new LinkedHashMap<>();
+
+        List<DeviceGroupMember> familyMembers = deviceGroupMemberMapper.selectList(
+                new LambdaQueryWrapper<DeviceGroupMember>()
+                        .eq(DeviceGroupMember::getAppUserId, appUserId)
+                        .eq(DeviceGroupMember::getStatus, STATUS_ENABLED)
+                        .orderByDesc(DeviceGroupMember::getJoinTime)
+        );
+        for (DeviceGroupMember member : familyMembers) {
+            DeviceGroup group = deviceGroupMapper.selectById(member.getGroupId());
+            if (group == null || !Integer.valueOf(STATUS_ENABLED).equals(group.getStatus())) {
+                continue;
+            }
+            Device device = findDeviceByCode(group.getDeviceCode());
+            if (device == null || !isFamilyDevice(device)) {
+                continue;
+            }
+            UserProfile profile = userProfileMapper.selectById(member.getProfileId());
+            result.put("F:" + device.getDeviceCode(), buildDeviceListItem(device, group, member, profile));
+        }
+
+        List<UserProfile> profiles = userProfileMapper.selectList(
+                new LambdaQueryWrapper<UserProfile>().eq(UserProfile::getUserId, appUserId)
+        );
+        if (!CollectionUtils.isEmpty(profiles)) {
+            List<Long> profileIds = profiles.stream().map(UserProfile::getId).collect(Collectors.toList());
+            List<UserDevice> bindings = userDeviceMapper.selectList(
+                    new LambdaQueryWrapper<UserDevice>()
+                            .in(UserDevice::getUserId, profileIds)
+                            .eq(UserDevice::getStatus, STATUS_ENABLED)
+                            .orderByDesc(UserDevice::getBindTime)
+            );
+            Map<Long, UserProfile> profileMap = profiles.stream()
+                    .collect(Collectors.toMap(UserProfile::getId, p -> p, (a, b) -> a));
+            for (UserDevice binding : bindings) {
+                Device device = findDeviceByCode(binding.getDeviceCode());
+                if (device == null || !isPublicDevice(device)) {
+                    continue;
+                }
+                UserProfile profile = profileMap.get(binding.getUserId());
+                result.put("P:" + device.getDeviceCode(), buildDeviceListItem(device, null, null, profile));
+            }
+        }
+
+        return new ArrayList<>(result.values());
+    }
+
+    @Override
+    public Map<String, Object> selectDevice(Long appUserId, AppDeviceSelectDTO dto) {
+        requireAppUser(appUserId);
+        Device device = requireDevice(dto.getDeviceCode());
+        if (isFamilyDevice(device)) {
+            DeviceGroup group = dto.getGroupId() != null
+                    ? requireGroup(dto.getGroupId())
+                    : requireDeviceGroup(device.getDeviceCode());
+            if (!device.getDeviceCode().equals(group.getDeviceCode())) {
+                throw new BusinessException(ResultCode.FORBIDDEN.getCode(), "无权使用该家庭设备");
+            }
+            DeviceGroupMember currentMember = requireCurrentAccountGroupMember(group.getId(), appUserId);
+            DeviceGroupMember selectedMember = currentMember;
+            if (dto.getProfileId() != null) {
+                selectedMember = findActiveMemberByProfileId(group.getId(), dto.getProfileId());
+                if (selectedMember == null) {
+                    throw new BusinessException(ResultCode.FORBIDDEN.getCode(), "所选成员不属于该群组");
+                }
+            }
+            UserProfile selectedProfile = requireProfile(selectedMember.getProfileId());
+            return buildDeviceContext(device, group, selectedMember, selectedProfile);
+        }
+
+        UserProfile selectedProfile = dto.getProfileId() != null
+                ? requireProfile(dto.getProfileId())
+                : ensureAccountProfile(requireAppUser(appUserId));
+        if (!Objects.equals(selectedProfile.getUserId(), appUserId)) {
+            throw new BusinessException(ResultCode.FORBIDDEN.getCode(), "公共设备只能选择当前账号档案");
+        }
+        if (!hasActiveUserDevice(selectedProfile.getId(), device.getDeviceCode())) {
+            throw new BusinessException(ResultCode.FORBIDDEN.getCode(), "请先绑定设备");
+        }
+        return buildDeviceContext(device, null, null, selectedProfile);
+    }
+
+    @Override
+    public List<Map<String, Object>> listGroupMembers(Long appUserId, Long groupId) {
+        DeviceGroup group = assertGroupAccess(groupId, appUserId);
+        List<DeviceGroupMember> members = deviceGroupMemberMapper.selectList(
+                new LambdaQueryWrapper<DeviceGroupMember>()
+                        .eq(DeviceGroupMember::getGroupId, group.getId())
+                        .eq(DeviceGroupMember::getStatus, STATUS_ENABLED)
+                        .orderByAsc(DeviceGroupMember::getJoinTime)
+        );
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (DeviceGroupMember member : members) {
+            UserProfile profile = userProfileMapper.selectById(member.getProfileId());
+            if (profile != null) {
+                result.add(buildMemberMap(member, profile));
+            }
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> addGroupMember(Long appUserId, Long groupId, AppGroupMemberDTO dto) {
+        DeviceGroup group = assertGroupAccess(groupId, appUserId);
+        validateFingerWidths(dto.getGender(), dto.getFingerWidth1(), dto.getFingerWidth15(), dto.getFingerWidth3());
+        String phone = normalizePhone(dto.getPhone());
+        checkGroupPhoneAvailable(group.getId(), phone, null);
+
+        AppUser linkedAccount = ensureAccountForPhone(phone, dto.getName(), dto.getUserCategoryId());
+        checkGroupAppUserAvailable(group.getId(), linkedAccount == null ? null : linkedAccount.getId(), null);
+
+        UserProfile profile = resolveProfileForNewMember(dto, phone, linkedAccount);
+        DeviceGroupMember member = createGroupMember(group.getId(), profile.getId(), linkedAccount == null ? null : linkedAccount.getId());
+
+        Device device = requireDevice(group.getDeviceCode());
+        ensureUserDevice(profile, device);
+        regenerateIfComplete(profile);
+        return buildMemberMap(member, profile);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> updateGroupMember(Long appUserId, Long groupId, Long profileId, AppGroupMemberDTO dto) {
+        DeviceGroup group = assertGroupAccess(groupId, appUserId);
+        DeviceGroupMember member = findActiveMemberByProfileId(group.getId(), profileId);
+        if (member == null) {
+            throw new BusinessException(ResultCode.PROFILE_NOT_FOUND);
+        }
+        requireProfile(profileId);
+        validateFingerWidths(dto.getGender(), dto.getFingerWidth1(), dto.getFingerWidth15(), dto.getFingerWidth3());
+
+        String phone = normalizePhone(dto.getPhone());
+        checkGroupPhoneAvailable(group.getId(), phone, profileId);
+        AppUser linkedAccount = ensureAccountForPhone(phone, dto.getName(), dto.getUserCategoryId());
+        Long linkedAppUserId = linkedAccount == null ? null : linkedAccount.getId();
+        checkGroupAppUserAvailable(group.getId(), linkedAppUserId, profileId);
+
+        updateProfileFromGroupDto(profileId, dto, phone, linkedAppUserId);
+        updateMemberAppUser(member, linkedAppUserId);
+
+        UserProfile updatedProfile = requireProfile(profileId);
+        Device device = requireDevice(group.getDeviceCode());
+        ensureUserDevice(updatedProfile, device);
+        regenerateIfComplete(updatedProfile);
+        return buildMemberMap(member, updatedProfile);
+    }
+
+    @Override
+    public Map<String, Object> getCurrentProfile(Long appUserId) {
+        AppUser appUser = requireAppUser(appUserId);
+        UserProfile profile = ensureAccountProfile(appUser);
+        return buildCurrentProfileMap(appUser, profile);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> updateCurrentProfile(Long appUserId, AppProfileDTO dto) {
+        AppUser appUser = requireAppUser(appUserId);
+        if (StringUtils.hasText(dto.getNickname()) || StringUtils.hasText(dto.getAvatar())) {
+            AppUser updateUser = new AppUser();
+            updateUser.setId(appUser.getId());
+            if (StringUtils.hasText(dto.getNickname())) {
+                updateUser.setNickname(dto.getNickname().trim());
+                appUser.setNickname(updateUser.getNickname());
+            }
+            if (StringUtils.hasText(dto.getAvatar())) {
+                updateUser.setAvatar(dto.getAvatar().trim());
+                appUser.setAvatar(updateUser.getAvatar());
+            }
+            appUserMapper.updateById(updateUser);
+        }
+
+        UserProfile profile = ensureAccountProfile(appUser);
+        validateFingerWidths(dto.getGender(), dto.getFingerWidth1(), dto.getFingerWidth15(), dto.getFingerWidth3());
+        UserProfile updateProfile = new UserProfile();
+        updateProfile.setId(profile.getId());
+        boolean changed = fillNonNullProfileFields(updateProfile, dto);
+        if (changed) {
+            userProfileMapper.updateById(updateProfile);
+            profile = requireProfile(profile.getId());
+            regenerateIfComplete(profile);
+        }
+        return buildCurrentProfileMap(appUser, profile);
+    }
+
+    private AppUser requireAppUser(Long appUserId) {
+        AppUser appUser = appUserMapper.selectById(appUserId);
+        if (appUser == null) {
+            throw new BusinessException(ResultCode.APP_USER_NOT_FOUND);
+        }
+        if (Integer.valueOf(0).equals(appUser.getStatus())) {
+            throw new BusinessException(ResultCode.APP_USER_DISABLED);
+        }
+        return appUser;
+    }
+
+    private Device requireDevice(String deviceCode) {
+        Device device = findDeviceByCode(deviceCode);
+        if (device == null) {
+            throw new BusinessException(ResultCode.DEVICE_NOT_FOUND);
+        }
+        return device;
+    }
+
+    private Device findDeviceByCode(String deviceCode) {
+        String code = trimToNull(deviceCode);
+        if (code == null) {
+            throw new BusinessException(ResultCode.PARAM_ERROR.getCode(), "设备编号不能为空");
+        }
+        return deviceMapper.selectOne(
+                new LambdaQueryWrapper<Device>()
+                        .eq(Device::getDeviceCode, code)
+                        .last("LIMIT 1")
+        );
+    }
+
+    private UserProfile requireProfile(Long profileId) {
+        UserProfile profile = userProfileMapper.selectById(profileId);
+        if (profile == null) {
+            throw new BusinessException(ResultCode.PROFILE_NOT_FOUND);
+        }
+        return profile;
+    }
+
+    private DeviceGroup requireGroup(Long groupId) {
+        DeviceGroup group = deviceGroupMapper.selectById(groupId);
+        if (group == null || !Integer.valueOf(STATUS_ENABLED).equals(group.getStatus())) {
+            throw new BusinessException(ResultCode.NOT_FOUND.getCode(), "家庭群组不存在");
+        }
+        return group;
+    }
+
+    private DeviceGroup requireDeviceGroup(String deviceCode) {
+        DeviceGroup group = findDeviceGroupByCode(deviceCode);
+        if (group == null) {
+            throw new BusinessException(ResultCode.NOT_FOUND.getCode(), "设备尚未建立家庭群组,请先绑定设备");
+        }
+        return group;
+    }
+
+    private DeviceGroup assertGroupAccess(Long groupId, Long appUserId) {
+        DeviceGroup group = requireGroup(groupId);
+        requireCurrentAccountGroupMember(group.getId(), appUserId);
+        return group;
+    }
+
+    private DeviceGroupMember requireCurrentAccountGroupMember(Long groupId, Long appUserId) {
+        DeviceGroupMember member = findActiveMemberByAppUserId(groupId, appUserId);
+        if (member == null) {
+            throw new BusinessException(ResultCode.FORBIDDEN.getCode(), "无权访问该家庭群组");
+        }
+        return member;
+    }
+
+    private DeviceGroup ensureDeviceGroup(Device device) {
+        DeviceGroup group = findDeviceGroupByCode(device.getDeviceCode());
+        if (group != null) {
+            return group;
+        }
+        group = new DeviceGroup();
+        group.setDeviceCode(device.getDeviceCode());
+        group.setStatus(STATUS_ENABLED);
+        deviceGroupMapper.insert(group);
+        return group;
+    }
+
+    private DeviceGroup findDeviceGroupByCode(String deviceCode) {
+        return deviceGroupMapper.selectOne(
+                new LambdaQueryWrapper<DeviceGroup>()
+                        .eq(DeviceGroup::getDeviceCode, deviceCode)
+                        .eq(DeviceGroup::getStatus, STATUS_ENABLED)
+                        .last("LIMIT 1")
+        );
+    }
+
+    private DeviceGroupMember createGroupMember(Long groupId, Long profileId, Long appUserId) {
+        DeviceGroupMember member = new DeviceGroupMember();
+        member.setGroupId(groupId);
+        member.setProfileId(profileId);
+        member.setAppUserId(appUserId);
+        member.setStatus(STATUS_ENABLED);
+        member.setJoinTime(LocalDateTime.now());
+        deviceGroupMemberMapper.insert(member);
+        return member;
+    }
+
+    private DeviceGroupMember findActiveMemberByProfileId(Long groupId, Long profileId) {
+        return deviceGroupMemberMapper.selectOne(
+                new LambdaQueryWrapper<DeviceGroupMember>()
+                        .eq(DeviceGroupMember::getGroupId, groupId)
+                        .eq(DeviceGroupMember::getProfileId, profileId)
+                        .eq(DeviceGroupMember::getStatus, STATUS_ENABLED)
+                        .last("LIMIT 1")
+        );
+    }
+
+    private DeviceGroupMember findActiveMemberByAppUserId(Long groupId, Long appUserId) {
+        return deviceGroupMemberMapper.selectOne(
+                new LambdaQueryWrapper<DeviceGroupMember>()
+                        .eq(DeviceGroupMember::getGroupId, groupId)
+                        .eq(DeviceGroupMember::getAppUserId, appUserId)
+                        .eq(DeviceGroupMember::getStatus, STATUS_ENABLED)
+                        .last("LIMIT 1")
+        );
+    }
+
+    private UserProfile ensureAccountProfile(AppUser appUser) {
+        UserProfile profile = userProfileMapper.selectOne(
+                new LambdaQueryWrapper<UserProfile>()
+                        .eq(UserProfile::getUserId, appUser.getId())
+                        .orderByAsc(UserProfile::getCreateTime)
+                        .last("LIMIT 1")
+        );
+        if (profile != null) {
+            return profile;
+        }
+
+        String phone = trimToNull(appUser.getPhone());
+        if (phone != null) {
+            profile = userProfileMapper.selectOne(
+                    new LambdaQueryWrapper<UserProfile>()
+                            .eq(UserProfile::getPhone, phone)
+                            .and(w -> w.isNull(UserProfile::getUserId).or().eq(UserProfile::getUserId, appUser.getId()))
+                            .orderByAsc(UserProfile::getCreateTime)
+                            .last("LIMIT 1")
+            );
+            if (profile != null) {
+                UserProfile update = new UserProfile();
+                update.setId(profile.getId());
+                update.setUserId(appUser.getId());
+                userProfileMapper.updateById(update);
+                profile.setUserId(appUser.getId());
+                return profile;
+            }
+        }
+
+        profile = new UserProfile();
+        profile.setUserId(appUser.getId());
+        profile.setPhone(phone);
+        profile.setName(resolveProfileName(appUser.getNickname(), phone, appUser.getId()));
+        profile.setUserType(NORMAL_USER_TYPE);
+        profile.setAddress(appUser.getAddress());
+        userProfileMapper.insert(profile);
+        return profile;
+    }
+
+    private AppUser ensureAccountForPhone(String phone, String name, Long userCategoryId) {
+        if (phone == null) {
+            return null;
+        }
+        validatePhone(phone);
+        AppUser user = appUserMapper.selectOne(
+                new LambdaQueryWrapper<AppUser>()
+                        .eq(AppUser::getPhone, phone)
+                        .last("LIMIT 1")
+        );
+        if (user != null) {
+            if (Integer.valueOf(0).equals(user.getStatus())) {
+                throw new BusinessException(ResultCode.APP_USER_DISABLED);
+            }
+            syncUserCategory(user, userCategoryId);
+            return user;
+        }
+
+        user = new AppUser();
+        user.setPhone(phone);
+        user.setNickname(resolveProfileName(name, phone, null));
+        user.setUserType(NORMAL_USER_TYPE);
+        user.setUserCategoryId(userCategoryId != null ? validateUserCategory(userCategoryId) : getDefaultUserCategoryId());
+        user.setStatus(STATUS_ENABLED);
+        appUserMapper.insert(user);
+        AppUser update = new AppUser();
+        update.setId(user.getId());
+        update.setAppId("AJY" + String.format("%08d", user.getId()));
+        appUserMapper.updateById(update);
+        user.setAppId(update.getAppId());
+        return user;
+    }
+
+    private void syncUserCategory(AppUser user, Long userCategoryId) {
+        if (userCategoryId == null || !Integer.valueOf(NORMAL_USER_TYPE).equals(user.getUserType())) {
+            return;
+        }
+        Long validCategoryId = validateUserCategory(userCategoryId);
+        if (validCategoryId.equals(user.getUserCategoryId())) {
+            return;
+        }
+        AppUser update = new AppUser();
+        update.setId(user.getId());
+        update.setUserCategoryId(validCategoryId);
+        appUserMapper.updateById(update);
+        user.setUserCategoryId(validCategoryId);
+    }
+
+    private Long validateUserCategory(Long userCategoryId) {
+        if (userCategoryId == null) {
+            return null;
+        }
+        long count = userCategoryMapper.selectCount(new LambdaQueryWrapper<UserCategory>()
+                .eq(UserCategory::getId, userCategoryId)
+                .eq(UserCategory::getUserType, NORMAL_USER_TYPE));
+        if (count <= 0) {
+            throw new BusinessException(ResultCode.USER_CATEGORY_NOT_FOUND);
+        }
+        return userCategoryId;
+    }
+
+    private Long getDefaultUserCategoryId() {
+        UserCategory category = userCategoryMapper.selectOne(new LambdaQueryWrapper<UserCategory>()
+                .eq(UserCategory::getUserType, NORMAL_USER_TYPE)
+                .eq(UserCategory::getName, "普通用户")
+                .last("LIMIT 1"));
+        if (category == null) {
+            category = userCategoryMapper.selectOne(new LambdaQueryWrapper<UserCategory>()
+                    .eq(UserCategory::getUserType, NORMAL_USER_TYPE)
+                    .orderByAsc(UserCategory::getId)
+                    .last("LIMIT 1"));
+        }
+        return category != null ? category.getId() : null;
+    }
+
+    private UserProfile buildProfileFromGroupDto(AppGroupMemberDTO dto, String phone, Long appUserId) {
+        UserProfile profile = new UserProfile();
+        profile.setUserId(appUserId);
+        profile.setName(dto.getName().trim());
+        profile.setGender(dto.getGender());
+        profile.setAge(dto.getAge());
+        profile.setPhone(phone);
+        profile.setUserType(NORMAL_USER_TYPE);
+        profile.setProvinceCode(trimToNull(dto.getProvinceCode()));
+        profile.setCityCode(trimToNull(dto.getCityCode()));
+        profile.setDistrictCode(trimToNull(dto.getDistrictCode()));
+        profile.setAddress(trimToNull(dto.getAddress()));
+        profile.setShoulderWidth(dto.getShoulderWidth());
+        profile.setBodyHeight(dto.getBodyHeight());
+        profile.setSpineLength(dto.getSpineLength());
+        profile.setHeight(dto.getHeight());
+        profile.setWeight(dto.getWeight());
+        profile.setFingerWidth1(dto.getFingerWidth1());
+        profile.setFingerWidth15(dto.getFingerWidth15());
+        profile.setFingerWidth3(dto.getFingerWidth3());
+        profile.setAcupointTableId(dto.getAcupointTableId());
+        return profile;
+    }
+
+    private UserProfile resolveProfileForNewMember(AppGroupMemberDTO dto, String phone, AppUser linkedAccount) {
+        if (linkedAccount == null) {
+            UserProfile profile = buildProfileFromGroupDto(dto, phone, null);
+            userProfileMapper.insert(profile);
+            return profile;
+        }
+
+        UserProfile profile = userProfileMapper.selectOne(
+                new LambdaQueryWrapper<UserProfile>()
+                        .eq(UserProfile::getUserId, linkedAccount.getId())
+                        .orderByAsc(UserProfile::getCreateTime)
+                        .last("LIMIT 1")
+        );
+        if (profile == null) {
+            profile = userProfileMapper.selectOne(
+                    new LambdaQueryWrapper<UserProfile>()
+                            .eq(UserProfile::getPhone, phone)
+                            .isNull(UserProfile::getUserId)
+                            .orderByAsc(UserProfile::getCreateTime)
+                            .last("LIMIT 1")
+            );
+        }
+        if (profile == null) {
+            profile = buildProfileFromGroupDto(dto, phone, linkedAccount.getId());
+            userProfileMapper.insert(profile);
+            return profile;
+        }
+
+        updateProfileFromGroupDto(profile.getId(), dto, phone, linkedAccount.getId());
+        return requireProfile(profile.getId());
+    }
+
+    private void updateProfileFromGroupDto(Long profileId, AppGroupMemberDTO dto, String phone, Long appUserId) {
+        LambdaUpdateWrapper<UserProfile> wrapper = new LambdaUpdateWrapper<UserProfile>()
+                .eq(UserProfile::getId, profileId)
+                .set(UserProfile::getUserId, appUserId)
+                .set(UserProfile::getName, dto.getName().trim())
+                .set(UserProfile::getGender, dto.getGender())
+                .set(UserProfile::getAge, dto.getAge())
+                .set(UserProfile::getPhone, phone)
+                .set(UserProfile::getProvinceCode, trimToNull(dto.getProvinceCode()))
+                .set(UserProfile::getCityCode, trimToNull(dto.getCityCode()))
+                .set(UserProfile::getDistrictCode, trimToNull(dto.getDistrictCode()))
+                .set(UserProfile::getAddress, trimToNull(dto.getAddress()))
+                .set(UserProfile::getShoulderWidth, dto.getShoulderWidth())
+                .set(UserProfile::getBodyHeight, dto.getBodyHeight())
+                .set(UserProfile::getSpineLength, dto.getSpineLength())
+                .set(UserProfile::getHeight, dto.getHeight())
+                .set(UserProfile::getWeight, dto.getWeight())
+                .set(UserProfile::getFingerWidth1, dto.getFingerWidth1())
+                .set(UserProfile::getFingerWidth15, dto.getFingerWidth15())
+                .set(UserProfile::getFingerWidth3, dto.getFingerWidth3())
+                .set(UserProfile::getAcupointTableId, dto.getAcupointTableId());
+        userProfileMapper.update(null, wrapper);
+    }
+
+    private boolean fillNonNullProfileFields(UserProfile profile, AppProfileDTO dto) {
+        boolean changed = false;
+        if (StringUtils.hasText(dto.getName())) {
+            profile.setName(dto.getName().trim());
+            changed = true;
+        }
+        if (dto.getGender() != null) {
+            profile.setGender(dto.getGender());
+            changed = true;
+        }
+        if (dto.getAge() != null) {
+            profile.setAge(dto.getAge());
+            changed = true;
+        }
+        String phone = normalizePhone(dto.getPhone());
+        if (phone != null) {
+            profile.setPhone(phone);
+            changed = true;
+        }
+        if (dto.getProvinceCode() != null) {
+            profile.setProvinceCode(trimToNull(dto.getProvinceCode()));
+            changed = true;
+        }
+        if (dto.getCityCode() != null) {
+            profile.setCityCode(trimToNull(dto.getCityCode()));
+            changed = true;
+        }
+        if (dto.getDistrictCode() != null) {
+            profile.setDistrictCode(trimToNull(dto.getDistrictCode()));
+            changed = true;
+        }
+        if (dto.getAddress() != null) {
+            profile.setAddress(trimToNull(dto.getAddress()));
+            changed = true;
+        }
+        if (dto.getShoulderWidth() != null) {
+            profile.setShoulderWidth(dto.getShoulderWidth());
+            changed = true;
+        }
+        if (dto.getBodyHeight() != null) {
+            profile.setBodyHeight(dto.getBodyHeight());
+            changed = true;
+        }
+        if (dto.getSpineLength() != null) {
+            profile.setSpineLength(dto.getSpineLength());
+            changed = true;
+        }
+        if (dto.getHeight() != null) {
+            profile.setHeight(dto.getHeight());
+            changed = true;
+        }
+        if (dto.getWeight() != null) {
+            profile.setWeight(dto.getWeight());
+            changed = true;
+        }
+        if (dto.getFingerWidth1() != null) {
+            profile.setFingerWidth1(dto.getFingerWidth1());
+            changed = true;
+        }
+        if (dto.getFingerWidth15() != null) {
+            profile.setFingerWidth15(dto.getFingerWidth15());
+            changed = true;
+        }
+        if (dto.getFingerWidth3() != null) {
+            profile.setFingerWidth3(dto.getFingerWidth3());
+            changed = true;
+        }
+        if (dto.getAcupointTableId() != null) {
+            profile.setAcupointTableId(dto.getAcupointTableId());
+            changed = true;
+        }
+        return changed;
+    }
+
+    private void updateMemberAppUser(DeviceGroupMember member, Long appUserId) {
+        LambdaUpdateWrapper<DeviceGroupMember> wrapper = new LambdaUpdateWrapper<DeviceGroupMember>()
+                .eq(DeviceGroupMember::getId, member.getId())
+                .set(DeviceGroupMember::getAppUserId, appUserId);
+        deviceGroupMemberMapper.update(null, wrapper);
+        member.setAppUserId(appUserId);
+    }
+
+    private void ensureUserDevice(UserProfile profile, Device device) {
+        UserDevice binding = userDeviceMapper.selectOne(
+                new LambdaQueryWrapper<UserDevice>()
+                        .eq(UserDevice::getUserId, profile.getId())
+                        .eq(UserDevice::getDeviceCode, device.getDeviceCode())
+                        .last("LIMIT 1")
+        );
+        if (binding == null) {
+            binding = new UserDevice();
+            binding.setUserId(profile.getId());
+            binding.setDeviceCode(device.getDeviceCode());
+            binding.setDeviceName(device.getDeviceName());
+            binding.setDeviceModel(device.getDeviceModel());
+            binding.setIsPrimary(0);
+            binding.setOnlineStatus(device.getOnlineStatus());
+            binding.setStatus(STATUS_ENABLED);
+            binding.setBindTime(LocalDateTime.now());
+            userDeviceMapper.insert(binding);
+            return;
+        }
+        LambdaUpdateWrapper<UserDevice> wrapper = new LambdaUpdateWrapper<UserDevice>()
+                .eq(UserDevice::getId, binding.getId())
+                .set(UserDevice::getDeviceName, device.getDeviceName())
+                .set(UserDevice::getDeviceModel, device.getDeviceModel())
+                .set(UserDevice::getOnlineStatus, device.getOnlineStatus())
+                .set(UserDevice::getStatus, STATUS_ENABLED);
+        userDeviceMapper.update(null, wrapper);
+    }
+
+    private boolean hasActiveUserDevice(Long profileId, String deviceCode) {
+        long count = userDeviceMapper.selectCount(
+                new LambdaQueryWrapper<UserDevice>()
+                        .eq(UserDevice::getUserId, profileId)
+                        .eq(UserDevice::getDeviceCode, deviceCode)
+                        .eq(UserDevice::getStatus, STATUS_ENABLED)
+        );
+        return count > 0;
+    }
+
+    private void checkGroupPhoneAvailable(Long groupId, String phone, Long excludeProfileId) {
+        if (phone == null) {
+            return;
+        }
+        List<DeviceGroupMember> members = deviceGroupMemberMapper.selectList(
+                new LambdaQueryWrapper<DeviceGroupMember>()
+                        .eq(DeviceGroupMember::getGroupId, groupId)
+                        .eq(DeviceGroupMember::getStatus, STATUS_ENABLED)
+        );
+        for (DeviceGroupMember member : members) {
+            if (Objects.equals(member.getProfileId(), excludeProfileId)) {
+                continue;
+            }
+            UserProfile profile = userProfileMapper.selectById(member.getProfileId());
+            if (profile != null && phone.equals(profile.getPhone())) {
+                throw new BusinessException(ResultCode.PHONE_PROFILE_EXISTS.getCode(), "同一群组内手机号不能重复");
+            }
+        }
+    }
+
+    private void checkGroupAppUserAvailable(Long groupId, Long appUserId, Long excludeProfileId) {
+        if (appUserId == null) {
+            return;
+        }
+        DeviceGroupMember member = findActiveMemberByAppUserId(groupId, appUserId);
+        if (member != null && !Objects.equals(member.getProfileId(), excludeProfileId)) {
+            throw new BusinessException(ResultCode.PHONE_PROFILE_EXISTS.getCode(), "该手机号对应账号已在群组中");
+        }
+    }
+
+    private void regenerateIfComplete(UserProfile profile) {
+        if (isProfileComplete(profile)) {
+            userAcupointService.generateForUser(profile.getId());
+        }
+    }
+
+    private boolean isProfileComplete(UserProfile profile) {
+        return profile != null
+                && StringUtils.hasText(profile.getName())
+                && profile.getGender() != null
+                && profile.getAge() != null
+                && profile.getShoulderWidth() != null
+                && profile.getBodyHeight() != null
+                && profile.getSpineLength() != null;
+    }
+
+    private void validateFingerWidths(Integer gender, BigDecimal fingerWidth1,
+                                      BigDecimal fingerWidth15, BigDecimal fingerWidth3) {
+        boolean male = Integer.valueOf(1).equals(gender);
+        boolean female = Integer.valueOf(2).equals(gender);
+        if (!male && !female) {
+            return;
+        }
+        checkFingerWidth("1寸", fingerWidth1,
+                male ? new BigDecimal("1.5") : new BigDecimal("1.3"),
+                male ? new BigDecimal("2.8") : new BigDecimal("2.4"));
+        checkFingerWidth("1.5寸", fingerWidth15,
+                male ? new BigDecimal("2.2") : new BigDecimal("2.0"),
+                male ? new BigDecimal("4.0") : new BigDecimal("3.5"));
+        checkFingerWidth("3寸", fingerWidth3,
+                male ? new BigDecimal("4.5") : new BigDecimal("4.0"),
+                male ? new BigDecimal("8.0") : new BigDecimal("7.0"));
+    }
+
+    private void checkFingerWidth(String label, BigDecimal value, BigDecimal min, BigDecimal max) {
+        if (value == null) {
+            return;
+        }
+        if (value.compareTo(min) < 0 || value.compareTo(max) > 0) {
+            throw new BusinessException(ResultCode.FINGER_WIDTH_RANGE_ERROR.getCode(),
+                    label + "指宽应在 " + min.toPlainString() + "~" + max.toPlainString() + " cm 之间");
+        }
+    }
+
+    private Map<String, Object> buildDeviceContext(Device device, DeviceGroup group,
+                                                   DeviceGroupMember member, UserProfile profile) {
+        Map<String, Object> data = buildDeviceListItem(device, group, member, profile);
+        data.put("device", buildDeviceMap(device));
+        data.put("group", group == null ? null : buildGroupMap(group));
+        data.put("member", member == null ? null : buildMemberMap(member, profile));
+        data.put("profile", buildProfileMap(profile));
+        data.put("profileComplete", isProfileComplete(profile));
+        return data;
+    }
+
+    private Map<String, Object> buildDeviceListItem(Device device, DeviceGroup group,
+                                                    DeviceGroupMember member, UserProfile profile) {
+        Map<String, Object> data = buildDeviceMap(device);
+        data.put("groupId", group == null ? null : group.getId());
+        data.put("memberId", member == null ? null : member.getId());
+        data.put("profileId", profile == null ? null : profile.getId());
+        data.put("profileName", profile == null ? null : profile.getName());
+        data.put("profileComplete", isProfileComplete(profile));
+        return data;
+    }
+
+    private Map<String, Object> buildDeviceMap(Device device) {
+        Map<String, Object> data = new HashMap<>(16);
+        data.put("id", device.getId());
+        data.put("deviceId", device.getId());
+        data.put("deviceCode", device.getDeviceCode());
+        data.put("deviceName", device.getDeviceName());
+        data.put("deviceModel", device.getDeviceModel());
+        data.put("deviceType", resolveDeviceType(device));
+        data.put("deviceTypeName", isPublicDevice(device) ? "公共设备" : "家庭设备");
+        data.put("serialNo", device.getSerialNo());
+        data.put("firmwareVersion", device.getFirmwareVersion());
+        data.put("onlineStatus", device.getOnlineStatus());
+        data.put("lastOnlineTime", device.getLastOnlineTime());
+        data.put("address", device.getAddress());
+        return data;
+    }
+
+    private Map<String, Object> buildGroupMap(DeviceGroup group) {
+        Map<String, Object> data = new HashMap<>(8);
+        data.put("id", group.getId());
+        data.put("groupId", group.getId());
+        data.put("deviceCode", group.getDeviceCode());
+        data.put("status", group.getStatus());
+        return data;
+    }
+
+    private Map<String, Object> buildMemberMap(DeviceGroupMember member, UserProfile profile) {
+        Map<String, Object> data = new HashMap<>(32);
+        data.put("memberId", member.getId());
+        data.put("groupId", member.getGroupId());
+        data.put("profileId", profile.getId());
+        data.put("appUserId", member.getAppUserId());
+        data.put("status", member.getStatus());
+        data.put("joinTime", member.getJoinTime());
+        data.put("profileComplete", isProfileComplete(profile));
+        data.putAll(buildProfileMap(profile));
+        data.put("profile", buildProfileMap(profile));
+        return data;
+    }
+
+    private Map<String, Object> buildProfileMap(UserProfile profile) {
+        Map<String, Object> data = new HashMap<>(32);
+        if (profile == null) {
+            return data;
+        }
+        data.put("profileId", profile.getId());
+        data.put("id", profile.getId());
+        data.put("appUserId", profile.getUserId());
+        data.put("name", profile.getName());
+        data.put("gender", profile.getGender());
+        data.put("age", profile.getAge());
+        data.put("phone", profile.getPhone());
+        data.put("userType", profile.getUserType());
+        data.put("provinceCode", profile.getProvinceCode());
+        data.put("cityCode", profile.getCityCode());
+        data.put("districtCode", profile.getDistrictCode());
+        data.put("address", profile.getAddress());
+        data.put("shoulderWidth", profile.getShoulderWidth());
+        data.put("bodyHeight", profile.getBodyHeight());
+        data.put("spineLength", profile.getSpineLength());
+        data.put("height", profile.getHeight());
+        data.put("weight", profile.getWeight());
+        data.put("fingerWidth1", profile.getFingerWidth1());
+        data.put("fingerWidth15", profile.getFingerWidth15());
+        data.put("fingerWidth3", profile.getFingerWidth3());
+        data.put("acupointTableId", profile.getAcupointTableId());
+        data.put("profileComplete", isProfileComplete(profile));
+        return data;
+    }
+
+    private Map<String, Object> buildCurrentProfileMap(AppUser appUser, UserProfile profile) {
+        Map<String, Object> data = buildProfileMap(profile);
+        data.put("userId", appUser.getId());
+        data.put("appUserId", appUser.getId());
+        data.put("nickname", appUser.getNickname());
+        data.put("avatar", appUser.getAvatar());
+        data.put("accountPhone", appUser.getPhone());
+        data.put("phone", profile.getPhone());
+        data.put("appId", appUser.getAppId());
+        data.put("userCategoryId", appUser.getUserCategoryId());
+        return data;
+    }
+
+    private boolean isFamilyDevice(Device device) {
+        return resolveDeviceType(device) == DEVICE_TYPE_FAMILY;
+    }
+
+    private boolean isPublicDevice(Device device) {
+        return resolveDeviceType(device) == DEVICE_TYPE_PUBLIC;
+    }
+
+    private int resolveDeviceType(Device device) {
+        return device.getDeviceType() == null ? DEVICE_TYPE_FAMILY : device.getDeviceType();
+    }
+
+    private String normalizePhone(String phone) {
+        String value = trimToNull(phone);
+        if (value != null) {
+            validatePhone(value);
+        }
+        return value;
+    }
+
+    private void validatePhone(String phone) {
+        if (phone != null && !phone.matches(PHONE_PATTERN)) {
+            throw new BusinessException(ResultCode.PHONE_FORMAT_INVALID);
+        }
+    }
+
+    private String trimToNull(String value) {
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        return value.trim();
+    }
+
+    private String resolveProfileName(String preferredName, String phone, Long fallbackId) {
+        String name = trimToNull(preferredName);
+        if (name != null) {
+            return name;
+        }
+        if (phone != null && phone.length() >= 4) {
+            return "用户" + phone.substring(phone.length() - 4);
+        }
+        return "用户" + (fallbackId == null ? "" : fallbackId);
+    }
+}

+ 12 - 0
code/backend/src/main/java/com/aijiuyi/admin/service/impl/DeviceServiceImpl.java

@@ -162,6 +162,7 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
         for (Device device : list) {
             long count = lambdaQuery().eq(Device::getDeviceCode, device.getDeviceCode()).count();
             if (count == 0) {
+                device.setDeviceType(resolveDeviceType(device.getDeviceType()));
                 device.setOnlineStatus(0);
                 save(device);
             }
@@ -301,6 +302,7 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
         device.setDeviceCode(dto.getDeviceCode());
         device.setDeviceName(dto.getDeviceName());
         device.setDeviceModel(dto.getDeviceModel());
+        device.setDeviceType(resolveDeviceType(dto.getDeviceType()));
         device.setSerialNo(dto.getSerialNo());
         device.setFirmwareVersion(dto.getFirmwareVersion());
         device.setProvinceCode(dto.getProvinceCode());
@@ -312,6 +314,16 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
         return device;
     }
 
+    private Integer resolveDeviceType(Integer deviceType) {
+        if (deviceType == null) {
+            return 1;
+        }
+        if (!Integer.valueOf(1).equals(deviceType) && !Integer.valueOf(2).equals(deviceType)) {
+            throw new BusinessException(ResultCode.PARAM_ERROR.getCode(), "设备类型不合法");
+        }
+        return deviceType;
+    }
+
     /**
      * 为设备批量绑定用户(新增时使用)
      *

+ 4 - 0
code/backend/src/main/resources/mapper/DeviceGroupMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.aijiuyi.admin.mapper.DeviceGroupMapper">
+</mapper>

+ 4 - 0
code/backend/src/main/resources/mapper/DeviceGroupMemberMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.aijiuyi.admin.mapper.DeviceGroupMemberMapper">
+</mapper>

+ 9 - 0
code/backend/src/main/resources/mapper/DeviceMapper.xml

@@ -14,6 +14,7 @@
             d.device_code,
             d.device_name,
             d.device_model,
+            COALESCE(d.device_type, 1) AS device_type,
             d.serial_no,
             d.firmware_version,
             d.online_status,
@@ -41,6 +42,14 @@
         <if test="q.deviceName != null and q.deviceName != ''">
             AND d.device_name LIKE CONCAT('%', #{q.deviceName}, '%')
         </if>
+        <choose>
+            <when test="q.deviceType != null and q.deviceType == 1">
+                AND (d.device_type = 1 OR d.device_type IS NULL)
+            </when>
+            <when test="q.deviceType != null and q.deviceType == 2">
+                AND d.device_type = 2
+            </when>
+        </choose>
         <if test="q.onlineStatus != null">
             AND d.online_status = #{q.onlineStatus}
         </if>

+ 16 - 0
code/backend/src/main/resources/rebel.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  This is the JRebel configuration file. It maps the running application to your IDE workspace, enabling JRebel reloading for this project.
+  Refer to https://manuals.jrebel.com/jrebel/standalone/config.html for more information.
+-->
+<application generated-by="intellij" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.zeroturnaround.com" xsi:schemaLocation="http://www.zeroturnaround.com http://update.zeroturnaround.com/jrebel/rebel-2_3.xsd">
+
+	<id>aijiuyi-admin</id>
+
+	<classpath>
+		<dir name="D:/Java_code/aijiuyi/code/backend/target/classes">
+		</dir>
+	</classpath>
+
+</application>

+ 31 - 0
code/backend/src/main/resources/sql/migration_device_group.sql

@@ -0,0 +1,31 @@
+-- 家庭/公共设备、设备群组与群组成员结构迁移
+ALTER TABLE `device`
+    ADD COLUMN `device_type` TINYINT NOT NULL DEFAULT 1 COMMENT '设备类型:1=家庭设备,2=公共设备' AFTER `device_model`;
+
+CREATE TABLE IF NOT EXISTS `device_group` (
+    `id` BIGINT NOT NULL COMMENT '主键ID',
+    `device_code` VARCHAR(64) NOT NULL COMMENT '家庭设备编号',
+    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1=正常,0=停用',
+    `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除',
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_device_group_device_code` (`device_code`),
+    KEY `idx_device_group_status` (`status`, `deleted`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭设备群组表';
+
+CREATE TABLE IF NOT EXISTS `device_group_member` (
+    `id` BIGINT NOT NULL COMMENT '主键ID',
+    `group_id` BIGINT NOT NULL COMMENT '群组ID',
+    `profile_id` BIGINT NOT NULL COMMENT '用户档案ID',
+    `app_user_id` BIGINT NULL COMMENT '关联App账号ID,可为空',
+    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1=正常,0=移除',
+    `join_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
+    `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除',
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_device_group_member_profile` (`group_id`, `profile_id`),
+    KEY `idx_device_group_member_app_user` (`group_id`, `app_user_id`, `status`),
+    KEY `idx_device_group_member_status` (`status`, `deleted`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭设备群组成员表';

+ 46 - 0
code/frontend/src/views/device/index.vue

@@ -12,6 +12,12 @@
         <el-form-item label="绑定用户">
           <el-input v-model="query.boundUserName" placeholder="用户姓名模糊搜索" clearable style="width:160px" />
         </el-form-item>
+        <el-form-item label="设备类型">
+          <el-select v-model="query.deviceType" placeholder="全部" clearable style="width:120px">
+            <el-option label="家庭设备" :value="1" />
+            <el-option label="公共设备" :value="2" />
+          </el-select>
+        </el-form-item>
         <el-form-item label="在线状态">
           <el-select v-model="query.onlineStatus" placeholder="全部" clearable style="width:110px">
             <el-option label="在线" :value="1" />
@@ -101,6 +107,13 @@
         <el-table-column prop="deviceModel" label="设备型号" width="90">
           <template #default="{ row }">{{ row.deviceModel || '-' }}</template>
         </el-table-column>
+        <el-table-column label="设备类型" width="95" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.deviceType === 2 ? 'warning' : 'success'" size="small">
+              {{ formatDeviceType(row.deviceType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
         <el-table-column label="绑定用户数" width="95" align="center">
           <template #default="{ row }">
             <el-tag type="info" size="small">{{ row.boundUserCount || 0 }} 人</el-tag>
@@ -190,6 +203,17 @@
             <el-option label="AJY-2025" value="AJY-2025" />
           </el-select>
         </el-form-item>
+        <el-form-item label="设备类型" prop="deviceType">
+          <el-select
+            v-model="deviceForm.deviceType"
+            placeholder="请选择设备类型"
+            style="width:100%"
+            :disabled="formMode === 'view'"
+          >
+            <el-option label="家庭设备" :value="1" />
+            <el-option label="公共设备" :value="2" />
+          </el-select>
+        </el-form-item>
         <el-form-item label="设备序列号" prop="serialNo">
           <el-input
             v-model="deviceForm.serialNo"
@@ -308,6 +332,7 @@ const query = reactive({
   deviceCode: '',
   deviceName: '',
   boundUserName: '',
+  deviceType: null,
   onlineStatus: null,
   firmwareVersion: '',
   provinceCode: '',
@@ -427,11 +452,16 @@ function formatBoundUserNames(row) {
     : displayText
 }
 
+function formatDeviceType(deviceType) {
+  return Number(deviceType) === 2 ? '公共设备' : '家庭设备'
+}
+
 /** 导出表头配置 */
 const exportHeaders = [
   { label: '设备编号', prop: 'deviceCode' },
   { label: '设备名称', prop: 'deviceName' },
   { label: '设备型号', prop: 'deviceModel' },
+  { label: '设备类型', prop: 'deviceTypeLabel' },
   { label: '所在地区', prop: 'regionText' },
   { label: '绑定用户数', prop: 'boundUserCount' },
   { label: '绑定用户', prop: 'boundUserNames' },
@@ -455,6 +485,7 @@ async function loadData() {
     // 为导出添加状态标签字段和地区文本
     tableData.value = records.map(r => ({
       ...r,
+      deviceTypeLabel: formatDeviceType(r.deviceType),
       onlineStatusLabel: r.onlineStatus === 1 ? '在线' : '离线',
       regionText: formatRegion(r)
     }))
@@ -480,6 +511,7 @@ function handleReset() {
     deviceCode: '',
     deviceName: '',
     boundUserName: '',
+    deviceType: null,
     onlineStatus: null,
     firmwareVersion: '',
     provinceCode: '',
@@ -508,6 +540,7 @@ const deviceForm = reactive({
   deviceCode: '',
   deviceName: '',
   deviceModel: '',
+  deviceType: 1,
   serialNo: '',
   firmwareVersion: '',
   userIds: [],
@@ -526,6 +559,7 @@ const deviceRules = {
     { max: 100, message: '设备名称不超过100个字符', trigger: 'blur' }
   ],
   deviceModel: [{ required: true, message: '请选择设备型号', trigger: 'change' }],
+  deviceType: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
   serialNo: [
     { required: true, message: '请输入设备序列号', trigger: 'blur' },
     { max: 50, message: '序列号不超过50个字符', trigger: 'blur' }
@@ -561,6 +595,7 @@ async function handleAdd() {
     deviceCode: '',
     deviceName: '',
     deviceModel: '',
+    deviceType: 1,
     serialNo: '',
     firmwareVersion: '',
     userIds: [],
@@ -585,6 +620,7 @@ async function handleView(row) {
   userOptions.value = boundUsers.map(u => ({ id: u.userId, name: u.userName, phone: u.phone }))
   Object.assign(deviceForm, {
     ...row,
+    deviceType: row.deviceType || 1,
     userIds: boundUsers.map(u => u.userId),
     regionCodes: [row.provinceCode, row.cityCode, row.districtCode].filter(Boolean),
     address: row.address || ''
@@ -614,6 +650,7 @@ async function handleEdit(row) {
     deviceCode: row.deviceCode,
     deviceName: row.deviceName,
     deviceModel: row.deviceModel || '',
+    deviceType: row.deviceType || 1,
     serialNo: row.serialNo || '',
     firmwareVersion: row.firmwareVersion || '',
     remark: row.remark || '',
@@ -637,6 +674,7 @@ async function submitForm() {
       deviceCode: deviceForm.deviceCode,
       deviceName: deviceForm.deviceName,
       deviceModel: deviceForm.deviceModel,
+      deviceType: deviceForm.deviceType || 1,
       serialNo: deviceForm.serialNo,
       firmwareVersion: deviceForm.firmwareVersion,
       provinceCode: codes[0] || '',
@@ -703,6 +741,7 @@ async function handleImport(data) {
     deviceCode: row['设备编号'] || row.deviceCode,
     deviceName: row['设备名称'] || row.deviceName,
     deviceModel: row['设备型号'] || row.deviceModel,
+    deviceType: parseDeviceType(row['设备类型'] || row.deviceType),
     serialNo: row['设备序列号'] || row.serialNo,
     firmwareVersion: row['固件版本'] || row.firmwareVersion,
     remark: row['备注'] || row.remark
@@ -710,6 +749,13 @@ async function handleImport(data) {
   await batchImportDevice(list)
 }
 
+function parseDeviceType(value) {
+  if (value === 2 || value === '2' || value === '公共设备') {
+    return 2
+  }
+  return 1
+}
+
 /**
  * 导入成功回调
  */