Эх сурвалжийг харах

fix(checkin): 健康打卡页 memberId 解析对齐首页,修复无任务进入时"成员ID无效"无法保存

根因:diet/exercise/sleep-checkin 的 onLoad 用
  if (role !== "child") { memberId = options.memberId || currentMemberId || null }
与首页 growth-main 的 currentChildId || currentMemberId || userId 解析不一致:
1) role==="child" 时 memberId 恒为 null(子用户打卡页拿不到成员)
2) parent 角色缺 currentChildId / userId 兜底。

触发路径:首页 handleProgressClick 仅在 item.taskId 存在(当日有待完成任务)时
拼 memberId 参数;无任务时 URL 不带 memberId → 打卡页 memberId 为空 →
提交被 if (!memberId) 拦截提示"成员ID无效",上传图片也无法保存
(后端 currentMemberId JWT 兜底到不了——前端提交前就拦截)。

修复:memberNormalize.js 新增 resolveCheckinMemberId 纯函数,
统一解析 options.memberId > currentChildId > currentMemberId > userId > null,
不再受 role 限制;三个打卡页 onLoad 共用。

测试:新增 7 用例(URL 优先/多级兜底/child 角色/全空),全部通过;
全量 jest 无新增失败(4 个既有失败与本改动无关)。
Xiaogang Liao 1 долоо хоног өмнө
parent
commit
e3c42553c4

+ 70 - 0
cfc-frontend/__tests__/utils/memberNormalize.resolveCheckin.test.js

@@ -0,0 +1,70 @@
+/**
+ * Tests for cfc-frontend utils/memberNormalize.js — resolveCheckinMemberId
+ *
+ * 背景:三个健康打卡页(diet/exercise/sleep-checkin)onLoad 里
+ *   `if (role !== 'child') { memberId = options.memberId || currentMemberId || null }`
+ *   与首页 growth-main 的 `currentChildId || currentMemberId || userId` 解析不一致:
+ *   1) role==='child' 时 memberId 恒为 null(打卡页根本拿不到成员)
+ *   2) parent 角色缺 currentChildId / userId 兜底
+ *   导致:无当日未完成任务(URL 不带 memberId)+ currentMemberId 为空时,
+ *   提交被 `if (!memberId)` 拦截提示"成员ID无效"。
+ *
+ * 本测试规定 resolveCheckinMemberId 的统一解析行为(与首页 growth-main 对齐):
+ *   options.memberId > currentChildId > currentMemberId > userId > null
+ *   不再受 role 限制。
+ */
+
+describe('resolveCheckinMemberId', () => {
+  let normalize
+
+  beforeAll(() => {
+    normalize = require('@/utils/memberNormalize')
+  })
+
+  test('URL 参数 options.memberId 优先(首页 tile 带 taskId 进入)', () => {
+    expect(normalize.resolveCheckinMemberId(
+      { memberId: '21', taskId: '4040' },
+      { currentChildId: '10', currentMemberId: '9', userId: '12' }
+    )).toBe('21')
+  })
+
+  test('无 URL 参数时回退 currentChildId(parent 切到孩子视角)', () => {
+    expect(normalize.resolveCheckinMemberId(
+      {},
+      { currentChildId: '21', currentMemberId: '', userId: '12' }
+    )).toBe('21')
+  })
+
+  test('再回退 currentMemberId', () => {
+    expect(normalize.resolveCheckinMemberId(
+      {},
+      { currentChildId: '', currentMemberId: '21', userId: '12' }
+    )).toBe('21')
+  })
+
+  test('最后回退 userId(首页与打卡页解析一致的兜底)', () => {
+    expect(normalize.resolveCheckinMemberId(
+      {},
+      { currentChildId: '', currentMemberId: '', userId: '12' }
+    )).toBe('12')
+  })
+
+  test('child 角色不返回 null(修复原 if(role!=="child") 恒 null 缺陷)', () => {
+    expect(normalize.resolveCheckinMemberId(
+      {},
+      { currentRole: 'child', currentChildId: '21', currentMemberId: '', userId: '12' }
+    )).toBe('21')
+  })
+
+  test('child 角色无 currentChildId 时回退 userId', () => {
+    expect(normalize.resolveCheckinMemberId(
+      {},
+      { currentRole: 'child', currentChildId: '', currentMemberId: '', userId: '12' }
+    )).toBe('12')
+  })
+
+  test('全部为空返回 null(保持提交拦截语义,由后端/调用方给出明确提示)', () => {
+    expect(normalize.resolveCheckinMemberId({}, {})).toBeNull()
+    expect(normalize.resolveCheckinMemberId(null, null)).toBeNull()
+  })
+})

+ 7 - 4
cfc-frontend/pages/growth/diet-checkin.vue

@@ -161,6 +161,7 @@
 
 <script>
 import config from '../../config'
+import { resolveCheckinMemberId } from '../../utils/memberNormalize.js'
 import { parseDate } from '../../utils/format.js'
 import { sttTranscribe, recognizeDietFood, getAllQuotaStatus } from '../../utils/api.js'
 import shareMixin from '../../components/share-mixin.js'
@@ -229,10 +230,12 @@ export default {
     this.taskId = options.taskId || null
     this.todayText = this.getToday()
     this.setShareInfo('我在用浠艾福记录家庭成员的每日饮食,一起来打卡吧', '/pages/growth/diet-checkin')
-    var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-    if (role !== 'child') {
-      this.memberId = options.memberId || uni.getStorageSync('currentMemberId') || null
-    }
+    this.memberId = resolveCheckinMemberId(options, {
+      currentRole: uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent',
+      currentChildId: uni.getStorageSync('currentChildId'),
+      currentMemberId: uni.getStorageSync('currentMemberId'),
+      userId: uni.getStorageSync('userId')
+    })
     // 支持从 records 页传 date 参数(补打)
     if (options.date) {
       this.form.recordDate = options.date

+ 7 - 4
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -150,6 +150,7 @@
 
 <script>
 import config from '../../config'
+import { resolveCheckinMemberId } from '../../utils/memberNormalize.js'
 import { parseDate } from '../../utils/format.js'
 import { sttTranscribe } from '../../utils/api.js'
 import { pickImage } from '../../utils/image-picker.js'
@@ -211,10 +212,12 @@ export default {
   onLoad: function(options) {
     this.taskId = options.taskId || null
     this.todayText = this.getToday()
-    var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-    if (role !== 'child') {
-      this.memberId = options.memberId || uni.getStorageSync('currentMemberId') || null
-    }
+    this.memberId = resolveCheckinMemberId(options, {
+      currentRole: uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent',
+      currentChildId: uni.getStorageSync('currentChildId'),
+      currentMemberId: uni.getStorageSync('currentMemberId'),
+      userId: uni.getStorageSync('userId')
+    })
     this.isSelfCheckin = uni.getStorageSync('currentView') !== 'member'
     this.recorderManager = uni.getRecorderManager()
     this.recorderManager.onStart(() => {

+ 7 - 4
cfc-frontend/pages/growth/sleep-checkin.vue

@@ -158,6 +158,7 @@
 
 <script>
 import config from '../../config'
+import { resolveCheckinMemberId } from '../../utils/memberNormalize.js'
 import { formatDate as formatDateUtil } from '../../utils/format.js'
 import { BrandColors } from '../../utils/brand-colors.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
@@ -297,10 +298,12 @@ export default {
     var day = ('0' + d.getDate()).slice(-2)
     this.todayDate = d.getFullYear() + '-' + m2 + '-' + day
     this.todayWeek = '周' + weekdays[d.getDay()]
-    var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-    if (role !== 'child') {
-      this.memberId = options.memberId || uni.getStorageSync('currentMemberId') || null
-    }
+    this.memberId = resolveCheckinMemberId(options, {
+      currentRole: uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent',
+      currentChildId: uni.getStorageSync('currentChildId'),
+      currentMemberId: uni.getStorageSync('currentMemberId'),
+      userId: uni.getStorageSync('userId')
+    })
     this.loadHistory()
   },
   onReady: function () {

+ 40 - 0
cfc-frontend/utils/memberNormalize.js

@@ -252,3 +252,43 @@ export function membersExceptSelf(members) {
 }
 
 export default normalizeMembers
+
+
+/**
+ * 健康打卡页(饮食/运动/睡眠)解析当前打卡成员(memberId),与首页 growth-main 的
+ * `currentChildId || currentMemberId || userId` 解析逻辑对齐。
+ *
+ * 背景(生产缺陷):三个打卡页 onLoad 原先用
+ *   `if (role !== 'child') { memberId = options.memberId || currentMemberId || null }`
+ *   与首页解析不一致:
+ *   1) role==='child' 时 memberId 恒为 null(子用户打卡页拿不到成员);
+ *   2) parent 角色缺 currentChildId / userId 兜底。
+ *   当从首页点击进入且当日无未完成任务(handleProgressClick 仅在 item.taskId 存在时
+ *   拼 memberId 参数,无任务时 URL 不带 memberId)时,memberId 为空 → 提交被
+ *   `if (!memberId)` 拦截提示"成员ID无效"。
+ *
+ * 优先级:options.memberId(URL/页面参数)> currentChildId > currentMemberId > userId > null。
+ * 不再受 role 限制(child 角色同样回退到 currentChildId/userId)。
+ *
+ * @param {Object|null} options  页面 onLoad 参数(含 memberId/taskId)
+ * @param {Object|null} storage  uni.getStorageSync 的可选快照(含 currentChildId/currentMemberId/userId/currentRole)
+ * @returns {String|Number|null} 解析出的 memberId;无法解析时返回 null(由调用方决定是否拦截)
+ */
+export function resolveCheckinMemberId(options, storage) {
+  var opts = options || {}
+  var st = storage || {}
+  var memberId = opts.memberId
+  if (memberId === null || memberId === undefined || memberId === '') {
+    memberId = st.currentChildId
+  }
+  if (memberId === null || memberId === undefined || memberId === '') {
+    memberId = st.currentMemberId
+  }
+  if (memberId === null || memberId === undefined || memberId === '') {
+    memberId = st.userId
+  }
+  if (memberId === null || memberId === undefined || memberId === '') {
+    return null
+  }
+  return memberId
+}