Procházet zdrojové kódy

feat(family): 多管理员——成员管理支持设置/取消家庭管理员

- AddFamilyMemberDTO 加 isAdmin 字段,/api/family/member/update 支持解析
- FamilyMemberService.updateMember:仅管理员(is_admin=1 或创建者)可设置管理员;
  取消管理员时家庭至少保留一名(校验统计 is_admin=1 数量)
- 操作者权限判定统一走 isFamilyAdminOperator(支持非创建者管理员)
- updateParentMember(家长账号)明确拒绝设置管理员
- 小程序 add-member.vue 编辑页加管理员开关(仅管理员可见、编辑模式显示);
  family-members.vue 列表加管理员徽标并透传 isAdmin/operatorAdmin

fix(frontend): sleep-checkin.vue 样式行尾多余 ) 导致构建失败(源自 e4e9c6c9d transition 提交)
Xiaogang Liao před 6 dny
rodič
revize
76759de54c

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java

@@ -175,6 +175,9 @@ public Result<FamilyMemberVO> updateMember(@RequestBody Map<String, Object> body
  if (body.get("birthday") != null) dto.setBirthday(body.get("birthday").toString());
  if (body.get("generationLevel") != null) dto.setGenerationLevel(body.get("generationLevel").toString());
  if (body.get("peerType") != null) dto.setPeerType(body.get("peerType").toString());
+ if (body.get("isAdmin") != null) {
+   dto.setIsAdmin(Integer.parseInt(body.get("isAdmin").toString()));
+ }
 
  try {
    FamilyMemberVO vo = familyMemberService.updateMember(userId, memberId, dto);

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AddFamilyMemberDTO.java

@@ -44,4 +44,7 @@ public class AddFamilyMemberDTO {
     /** 是否剖腹产: 0=顺产, 1=剖腹产 (选填) */
     private Integer isCesarean;
 
+    /** 是否家庭管理员(多管理员,仅编辑成员时生效): 0=否, 1=是;null=不修改 */
+    private Integer isAdmin;
+
 }

+ 44 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java

@@ -885,16 +885,31 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
             return updateParentMember(user, memberId, dto);
         }
 
-        // 检查编辑权限:管理员可编辑所有成员;普通用户只能编辑无关联账号的成员
-        boolean isAdmin = false;
-        Family family = familyMapper.selectById(user.getFamilyId());
-        if (family != null && Long.valueOf(userId).equals(family.getCreatorId())) {
-            isAdmin = true;
-        }
+        // 检查编辑权限:管理员(family_members.is_admin=1 或家庭创建者)可编辑所有成员;
+        // 普通用户只能编辑无关联账号的成员
+        boolean isAdmin = isFamilyAdminOperator(userId, user.getFamilyId());
         if (!isAdmin && !isEditable(memberId)) {
             throw new RuntimeException("该成员关联了登录账号,不可编辑");
         }
 
+        // 管理员设置(多管理员):仅管理员可操作;取消管理员时家庭至少保留一名
+        if (dto.getIsAdmin() != null) {
+            if (!isAdmin) {
+                throw new RuntimeException("仅家庭管理员可设置管理员");
+            }
+            int newAdminVal = dto.getIsAdmin() == 1 ? 1 : 0;
+            boolean targetIsAdmin = member.getIsAdmin() != null && member.getIsAdmin() == 1;
+            if (newAdminVal == 0 && targetIsAdmin) {
+                Long adminCount = familyMemberMapper.selectCount(new LambdaQueryWrapper<FamilyMember>()
+                        .eq(FamilyMember::getFamilyId, user.getFamilyId())
+                        .eq(FamilyMember::getIsAdmin, 1));
+                if (adminCount != null && adminCount <= 1) {
+                    throw new RuntimeException("家庭至少需要保留一名管理员");
+                }
+            }
+            member.setIsAdmin(newAdminVal);
+        }
+
         String oldNickname = member.getNickname();
         if (dto.getNickname() != null && !dto.getNickname().trim().isEmpty()) {
             member.setNickname(dto.getNickname().trim());
@@ -931,6 +946,26 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         return toFamilyMemberVO(member);
     }
 
+    /**
+     * 判断用户是否为家庭管理员(多管理员):家庭创建者,或其成员记录 is_admin=1。
+     */
+    private boolean isFamilyAdminOperator(Long userId, Long familyId) {
+        if (userId == null || familyId == null) return false;
+        Family family = familyMapper.selectById(familyId);
+        if (family != null && family.getCreatorId() != null && Long.valueOf(userId).equals(family.getCreatorId())) {
+            return true;
+        }
+        try {
+            Long c = familyMemberMapper.selectCount(new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, familyId)
+                    .eq(FamilyMember::getUserId, userId)
+                    .eq(FamilyMember::getIsAdmin, 1));
+            return c != null && c > 0;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
     /**
      * 更新家长账号(users 表成员)的基本资料。
      * 家长账号只存在于 users 表(family_id + role=parent),无 family_members 记录,
@@ -942,6 +977,9 @@ vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
         if (!isAdmin) {
             throw new RuntimeException("仅家庭创建者可编辑该成员");
         }
+        if (dto.getIsAdmin() != null) {
+            throw new RuntimeException("该成员为家长账号,不支持设置管理员,请对家庭成员记录操作");
+        }
         User parent = userMapper.selectById(memberId);
         if (parent == null
                 || parent.getFamilyId() == null

+ 21 - 1
cfc-frontend/pages/family/add-member.vue

@@ -82,6 +82,15 @@
 <text class="hint">若手机号已注册,将提示是否邀请该用户加入家庭</text>
 </view>
 
+<!-- 家庭管理员(编辑模式 + 操作者为管理员) -->
+<view class="form-item" v-if="isEdit && operatorAdmin">
+<view class="admin-row">
+<text class="admin-row-text">家庭管理员</text>
+<switch :checked="form.isAdmin === 1" color="#F97316" @change="onAdminChange" />
+</view>
+<text class="hint">{{ form.isAdmin === 1 ? '管理员可查看/管理全家庭报告与成员' : '仅可查看本人报告' }}</text>
+</view>
+
 <!-- 保存按钮 -->
 <button class="btn-primary" :loading="submitting" @click="submit">保存</button>
 
@@ -120,6 +129,7 @@ return {
 submitting: false,
 isEdit: false,
 editMemberId: null,
+operatorAdmin: false,
 form: {
 nickname: '',
 generationLevel: '',
@@ -132,7 +142,8 @@ birthHourName: '',
 birthWeight: '',
 birthPlace: '',
 isCesarean: null,
-cesareanLabel: ''
+cesareanLabel: '',
+isAdmin: 0
 },
 genderOptions: [
 { value: 'male', label: '男' },
@@ -179,9 +190,14 @@ onLoad(options) {
     if (options.birthday) this.form.birthday = options.birthday
     if (options.generationLevel) this.form.generationLevel = options.generationLevel
     if (options.peerType) this.form.peerType = options.peerType
+    if (options.isAdmin) this.form.isAdmin = Number(options.isAdmin)
+    if (options.operatorAdmin) this.operatorAdmin = Number(options.operatorAdmin) === 1
   }
 },
 methods: {
+onAdminChange(e) {
+this.form.isAdmin = e.detail.value ? 1 : 0
+},
 onGenderChange(e) {
 this.form.gender = this.genderOptions[e.detail.value].value
 },
@@ -296,6 +312,8 @@ if (this.isEdit) {
   if (editGen) updatePayload.generationLevel = editGen
   var editPeer = (this.$refs.generationPicker && this.$refs.generationPicker.selectedPeer) || this.form.peerType
   if (editPeer) updatePayload.peerType = editPeer
+  // 管理员设置(多管理员):操作者是管理员时随资料提交(后端校验至少保留一名管理员)
+  if (this.operatorAdmin) updatePayload.isAdmin = this.form.isAdmin === 1 ? 1 : 0
   var res = await updateFamilyMember(updatePayload)
   if (res.code === 200) {
     uni.showToast({ title: '已更新', icon: 'success' })
@@ -352,6 +370,8 @@ this.submitting = false
 .page-title { font-size: 34rpx; font-weight: bold; color: #333; margin-bottom: 30rpx; }
 .form-item { margin-bottom: 30rpx; }
 .form-item.required .label { font-weight: bold; }
+.admin-row { display: flex; align-items: center; justify-content: space-between; }
+.admin-row-text { font-size: 30rpx; color: #333; }
 .label { display: block; font-size: 28rpx; color: #333; margin-bottom: 10rpx; }
 .required-mark { color: #F97316; margin-right: 4rpx; }
 .input { border: 1rpx solid #ddd; border-radius: 10rpx; padding: 20rpx; font-size: 28rpx; }

+ 14 - 0
cfc-frontend/pages/profile-extra/family-members.vue

@@ -36,6 +36,7 @@
         <image class="member-avatar" :src="m.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
         <view class="member-info">
           <text class="member-name">{{ m.nickname || m.name || '成员' }}</text>
+          <text class="member-admin-tag" v-if="m.isAdmin">管理员</text>
           <text class="member-role-tag">{{ m.relationship || m.relativeLabel || m.roleLabel || '成员' }}</text>
         </view>
         <text class="member-arrow">›</text>
@@ -711,6 +712,9 @@ export default {
       // 始终传递辈分关系,保证编辑页回显并可修改(generation 为空时传空串,编辑页需用户重新选择)
       params += '&generationLevel=' + (this.generationToLevel(member.generation) || '')
       if (member.isSpouse) params += '&peerType=spouse'
+      // 管理员设置(多管理员):回显成员当前管理员状态 + 操作者是否为管理员
+      params += '&isAdmin=' + (member.isAdmin ? 1 : 0)
+      params += '&operatorAdmin=' + (this.isAdmin ? 1 : 0)
       uni.navigateTo({ url: '/pages/family/add-member?' + params })
     },
     doQuestionnaire(member) {
@@ -925,6 +929,16 @@ export default {
   display: block;
   color: #333;
 }
+.member-admin-tag {
+  display: inline-block;
+  font-size: 20rpx;
+  color: #6D28D9;
+  background: #EDE9FE;
+  padding: 2rpx 10rpx;
+  border-radius: 8rpx;
+  margin-top: 6rpx;
+  margin-right: 8rpx;
+}
 .member-role-tag {
   display: inline-block;
   font-size: 22rpx;