From c1857ce8528b516da0a6141657bcc3e6c02b8c8a Mon Sep 17 00:00:00 2001 From: msh-agent Date: Mon, 9 Mar 2026 18:56:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=85=B3=E6=B3=A8?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=9B=B8=E5=85=B3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 食谱详情页: 修复 applyDefaultData 中未定义变量 id 的问题 - 帖子详情页: 优化 toggleFollow 方法,提前校验 author.id,兼容多种后端字段 - 为帖子详情页已关注状态添加灰色样式 --- .DS_Store | Bin 10244 -> 8196 bytes msh_single_uniapp/.fixes/logs/auto-start.log | 1686 +++++++++++++++++ msh_single_uniapp/.fixes/logs/launchd.log | 1686 +++++++++++++++++ msh_single_uniapp/api/tool.js | 5 +- .../pages/tool/ai-nutritionist.vue | 9 +- .../pages/tool/calculator-result.vue | 12 +- msh_single_uniapp/pages/tool/checkin.vue | 55 +- msh_single_uniapp/pages/tool/food-detail.vue | 50 +- .../pages/tool/food-encyclopedia.vue | 49 +- .../pages/tool/nutrition-knowledge.vue | 40 +- msh_single_uniapp/pages/tool/post-detail.vue | 55 +- .../pages/tool/recipe-detail.vue | 14 +- .../pages/tool_main/community.vue | 23 +- scripts/run-t10-regression.sh | 8 + 14 files changed, 3590 insertions(+), 102 deletions(-) create mode 100644 scripts/run-t10-regression.sh diff --git a/.DS_Store b/.DS_Store index 04ef343768f8ae14ebcee6652abead37885b9b02..3a141f4687c5b347d43f4c3c43012b1688682e76 100644 GIT binary patch delta 498 zcmZn(XmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAF9(tb@);P48S)v5HpZ>xXOx>f zM?jG=bMig`Ii0#?4yW}N9CrjNV`tD~NM%};Sj%E?ax%5XqbP390(V%#uU zS;&!%fuW(_YtG~eLL!svgm@5KTjqeS=*bs^B-r+C(R?Vqa`HPNDHxMaSeiLdfMN1J z5t+#^g#WR~a^EiiaYSAL78w>VHn_?eq$xeRUgl1+!e!A7| zO7K-_>x&QiqV&=J_$ofQP)fxIiwG58bP?&3Ew)8`5Gy|Fe&A|VG0sLmu7M+$-A2kD-0nLEN0M!Q*cam8pyHbkMfd|n7pbX({vEe!P z0Yb-AGOJ`)N}-C+l-+}&R1MW)7*ZX_O@gD$D%q7%s)Hfb!BCbB)u9-|I_jAM4n|f= zebfwS28J1+-aP{Qp#*iXc>iuU2ps(@lpjXeZZ21>yF~&#vNJzUUAc8Q2vr^f)u_^H zfwUGoCX7mdI?#j`xX_gA2@(OuyLUZH9)kO`anm~W=V#0xhU)t>8pZtqV;zlQh1w6x zP=hx2Wa`Y_-}xB#;roq85073GK6tve+qWJx&h1W}7ga2bc5Aev0v5Ln@Z3*mnT>MW zyYH9aLuuQG=vxm)aQgt4C~nHbS>gjoM?(zW#JT8#18pead*Z^f&x4uU zwO|zY<;v5)oqJK{CE$6Slg*Jn|7JGGzZtMGOI7r(gnrsMUy691f>d#()ll#0H^oN1 z)@mmbKN#C4LgC3sG=_h((bM^=yOM8P?M`daYQ8~NuUhSDQJtS~-MV*f6<@DaRtj@Z z9C4ktRdXs$p6dUVF!n@oZ$oJTH5kll0?YB$j8# zj+tLRxGLV*P4NAhGY+0-YR`sA0gQb1*?iNfyUsFVDDItJMq5|`9IQTd%xVGavxCQe)WTFXKPcnqZz-Qfr&`tdeaPO z1~dbjfya-52^o?}>;Es0{{R2u7r2f{GoTsp7%)P)d~Ojhdo8y&qE2Y7-NbzfciITo zl~SnSfgp5WEXU&oEyq8r?j=alhT~Z!yHW~uyf4&I3U!onHUB^RXMk88;lHZK>F0U# fG7g^q+SdI) (part && part.text) ? part.text : '').filter(Boolean).join(''); } - if (typeof content === 'object' && Array.isArray(content.parts)) { - return content.parts.map(part => (part && part.text) ? part.text : '').filter(Boolean).join(''); + if (typeof content === 'object') { + if (Array.isArray(content.parts)) { + return content.parts.map(part => (part && part.text) ? part.text : '').filter(Boolean).join(''); + } + if (typeof content.text === 'string') return content.text; } return String(content); }, diff --git a/msh_single_uniapp/pages/tool/calculator-result.vue b/msh_single_uniapp/pages/tool/calculator-result.vue index 63a1b17..0cc42f0 100644 --- a/msh_single_uniapp/pages/tool/calculator-result.vue +++ b/msh_single_uniapp/pages/tool/calculator-result.vue @@ -489,19 +489,19 @@ export default { gap: 16rpx; transition: all 0.2s ease; box-sizing: border-box; - /* 未激活:明显变灰,无下划线 */ + /* 未激活:明显变灰,无下划线(BUG-002) */ color: #9ca3af; font-weight: 400; - border-bottom: none; + border-bottom: 3rpx solid transparent; .tab-icon { font-size: 28rpx; - color: #9ca3af; - font-weight: 400; + color: inherit; + font-weight: inherit; } .tab-text { font-size: 28rpx; - color: #9ca3af; - font-weight: 400; + color: inherit; + font-weight: inherit; } /* 激活:加粗、主色、橙色底部下划线(BUG-002) */ &.active { diff --git a/msh_single_uniapp/pages/tool/checkin.vue b/msh_single_uniapp/pages/tool/checkin.vue index 8aa0cf4..6fdcbc4 100644 --- a/msh_single_uniapp/pages/tool/checkin.vue +++ b/msh_single_uniapp/pages/tool/checkin.vue @@ -244,28 +244,16 @@ export default { const { setSignIntegral, getFrontUserInfo, getUserInfo } = await import('@/api/user.js'); const { getUserPoints } = await import('@/api/tool.js'); - // 子问题 A:不得在 API 返回成功前修改 currentPoints,避免打卡前积分提前跳变 - // 打卡接口:GET /api/front/user/sign/integral(setSignIntegral),等同于 checkin 签到 + // 子问题 A:在 API 返回成功前不得修改 currentPoints,避免打卡前积分提前跳变(禁止前端本地 +30 等) + // 打卡接口:GET /api/front/user/sign/integral(setSignIntegral),即本项目的签到/打卡接口 await setSignIntegral(); - // 子问题 B:仅在打卡成功后用服务端数据更新积分。先 GET /api/front/user/info 刷新用户积分,禁止硬编码 +30 - let userRes = null; - try { - userRes = await getFrontUserInfo(); // GET /api/front/user/info - } catch (_) { - userRes = await getUserInfo().catch(() => null); - } - if (userRes && userRes.data && (userRes.data.integral != null || userRes.data.points != null)) { - this.currentPoints = userRes.data.integral ?? userRes.data.points ?? 0; - } else { - const pointsRes = await getUserPoints(); - if (pointsRes && pointsRes.data) { - const serverPoints = pointsRes.data.totalPoints ?? pointsRes.data.points ?? pointsRes.data.availablePoints ?? 0; - this.currentPoints = serverPoints; - } + // 子问题 B:打卡成功后必须用服务端数据更新积分。发 GET /api/front/user/info 刷新用户积分,禁止硬编码 +30 + const serverPoints = await this._fetchServerPoints(getFrontUserInfo, getUserInfo, getUserPoints); + if (serverPoints != null) { + this.currentPoints = Number(serverPoints); } - // 积分已从服务端更新后再更新打卡状态并跳转,避免“已打卡”与积分不同步 this.todaySigned = true; } catch (e) { const msg = (typeof e === 'string' ? e : (e && (e.message || e.msg))) || '打卡失败'; @@ -279,6 +267,37 @@ export default { url: '/pages/tool/checkin-publish' }); }, + /** + * 从服务端拉取积分(用于打卡成功后刷新,积分值必须来自服务端,不可前端累加) + * 优先 GET /api/front/user/info,失败时回退到 user 或 tool/points/info + */ + async _fetchServerPoints(getFrontUserInfo, getUserInfo, getUserPoints) { + try { + const userRes = await getFrontUserInfo(); + if (userRes && userRes.data) { + const d = userRes.data; + const p = d.integral ?? d.points ?? d.totalPoints ?? (d.user && (d.user.integral ?? d.user.points ?? d.user.totalPoints)); + if (p != null) return p; + } + } catch (_) {} + try { + const userRes = await getUserInfo(); + if (userRes && userRes.data) { + const d = userRes.data; + const p = d.integral ?? d.points ?? d.totalPoints ?? (d.user && (d.user.integral ?? d.user.points ?? d.user.totalPoints)); + if (p != null) return p; + } + } catch (_) {} + try { + const pointsRes = await getUserPoints(); + if (pointsRes && pointsRes.data) { + const d = pointsRes.data; + const p = d.totalPoints ?? d.points ?? d.availablePoints; + if (p != null) return p; + } + } catch (_) {} + return null; + }, getTodayIndex() { // 根据连续打卡数据计算当前是第几天 // streakDays 中已有 completed 状态,找第一个未完成的位置即为今天 diff --git a/msh_single_uniapp/pages/tool/food-detail.vue b/msh_single_uniapp/pages/tool/food-detail.vue index 24fedae..bdb52de 100644 --- a/msh_single_uniapp/pages/tool/food-detail.vue +++ b/msh_single_uniapp/pages/tool/food-detail.vue @@ -133,9 +133,10 @@ export default { } }, computed: { - // 保证 .food-name-overlay / .nutrient-card / .nutrition-row 在 defaultFoodData 状态下也有非空数据可渲染 + // 保证 .food-name-overlay / .nutrient-card / .nutrition-row 在 defaultFoodData 状态下也有非空数据可渲染(不为空字符串/空数组) displayName() { - return (this.foodData && this.foodData.name) ? this.foodData.name : (this.defaultFoodData.name || '—') + const name = (this.foodData && this.foodData.name) ? this.foodData.name : (this.defaultFoodData && this.defaultFoodData.name) ? this.defaultFoodData.name : '—' + return (name != null && String(name).trim() !== '') ? String(name).trim() : '—' }, displayCategory() { return (this.foodData && this.foodData.category) ? this.foodData.category : (this.defaultFoodData.category || '—') @@ -149,14 +150,16 @@ export default { displayKeyNutrients() { const arr = this.foodData && this.foodData.keyNutrients if (Array.isArray(arr) && arr.length > 0) return arr - const def = this.defaultFoodData.keyNutrients - return Array.isArray(def) && def.length > 0 ? def : [{ name: '—', value: '—', unit: '', status: '—' }] + const def = this.defaultFoodData && this.defaultFoodData.keyNutrients + if (Array.isArray(def) && def.length > 0) return def + return [{ name: '—', value: '—', unit: '', status: '—' }] }, displayNutritionTable() { const arr = this.foodData && this.foodData.nutritionTable if (Array.isArray(arr) && arr.length > 0) return arr - const def = this.defaultFoodData.nutritionTable - return Array.isArray(def) && def.length > 0 ? def : [{ name: '—', value: '—', unit: '', level: 'low', levelText: '—' }] + const def = this.defaultFoodData && this.defaultFoodData.nutritionTable + if (Array.isArray(def) && def.length > 0) return def + return [{ name: '—', value: '—', unit: '', level: 'low', levelText: '—' }] } }, onLoad(options) { @@ -171,21 +174,23 @@ export default { name: (displayName !== undefined && displayName !== '') ? String(displayName) : '' } // 打印入参便于排查:后端详情接口仅接受 Long 类型 id,传 name 会导致 400 - console.log('[food-detail] onLoad options:', options) - console.log('[food-detail] onLoad pageParams (id/name):', this.pageParams) + console.log('[food-detail] onLoad options:', JSON.stringify(options)) + console.log('[food-detail] onLoad pageParams (id/name):', JSON.stringify(this.pageParams)) const isNumericId = rawId !== undefined && rawId !== '' && !isNaN(Number(rawId)) if (isNumericId) { const numId = Number(rawId) - console.log('[food-detail] 使用数字 id 请求详情,不传 name 字段:', numId) + console.log('[food-detail] 使用数字 id 请求详情:', { id: numId, idType: typeof numId, name: this.pageParams.name }) this.loadFoodData(numId) } else if (this.pageParams.name) { // 无有效 id、仅有 name 时,不请求接口(避免传 name 导致后端 NumberFormatException),直接展示默认数据 + 当前名称 + console.log('[food-detail] 无数字 id,仅用 name 展示默认数据,不请求详情接口') this.loadError = '暂无该食物详情数据,展示参考数据' this.applyDefaultFoodData(false) try { this.foodData.name = decodeURIComponent(this.pageParams.name) } catch (e) {} } else { + console.log('[food-detail] 无 id 与 name,展示默认数据') this.applyDefaultFoodData() } }, @@ -225,12 +230,26 @@ export default { async loadFoodData(id) { this.loading = true this.loadError = '' - // 打印 API 请求参数便于确认(后端需要 Long 类型 id) - console.log('[food-detail] getFoodDetail request param:', { id, type: typeof id }) + // 打印 API 请求参数便于确认(后端需要 Long 类型 id,传 name 会 400) + console.log('[food-detail] getFoodDetail API 请求参数:', { + id, + idType: typeof id, + pageParamsId: this.pageParams.id, + pageParamsName: this.pageParams.name + }) try { const res = await getFoodDetail(id) // 打印响应结构便于确认:request 成功时 resolve 的是 res.data,即 { code: 200, data: {...} } - console.log('[food-detail] getFoodDetail 响应结构:', res ? { hasData: !!res.data, code: res.code, keys: Object.keys(res || {}) } : null) + console.log('[food-detail] getFoodDetail 响应结构:', res ? { hasData: !!(res && res.data), code: res && res.code, keys: Object.keys(res || {}) } : null) + if (!res) { + this.loadError = '接口未返回数据' + this.applyDefaultFoodData(false) + if (this.pageParams && this.pageParams.name) { + try { this.foodData.name = decodeURIComponent(String(this.pageParams.name)) } catch (e) {} + } + uni.showToast({ title: '数据加载失败', icon: 'none' }) + return + } const data = res.data != null ? res.data : res console.log('[food-detail] getFoodDetail 解析后 data:', data ? { hasName: !!data.name, hasImage: !!data.image, keys: Object.keys(data) } : null) if (data && data.name) { @@ -258,9 +277,11 @@ export default { uni.showToast({ title: '数据加载失败', icon: 'none' }) } } catch (error) { - // a. 将 loadError 置为具体错误信息(用于调试):兼容 Error、{ message/msg }、字符串 + // a. 将 loadError 置为具体错误信息(用于调试):兼容 Error、{ message/msg }、字符串、后端 res.data 对象 const errMsg = (error && (error.message || error.msg || error.errMsg)) ? String(error.message || error.msg || error.errMsg) + : (error && error.data && (error.data.message || error.data.msg)) + ? String(error.data.message || error.data.msg) : (typeof error === 'string' ? error : (error ? String(error) : '未知错误')) console.error('[food-detail] 加载食物数据失败:', error) console.error('[food-detail] loadError(用于调试):', errMsg) @@ -273,7 +294,8 @@ export default { this.foodData.name = decodeURIComponent(String(this.pageParams.name)) } catch (e) {} } - // c. 页面通过 v-if="loadError" 显示「当前数据来自缓存,可能不是最新」;再弹出轻提示 + console.log('[food-detail] 已用 defaultFoodData 填充页面,loadError=', this.loadError) + // c. 页面 v-if="loadError" 会显示「当前数据来自缓存,可能不是最新」;同时弹出轻提示 uni.showToast({ title: '数据加载失败', icon: 'none' diff --git a/msh_single_uniapp/pages/tool/food-encyclopedia.vue b/msh_single_uniapp/pages/tool/food-encyclopedia.vue index 95b3176..7f37e6e 100644 --- a/msh_single_uniapp/pages/tool/food-encyclopedia.vue +++ b/msh_single_uniapp/pages/tool/food-encyclopedia.vue @@ -269,30 +269,35 @@ export default { console.error('加载食物列表失败:', error); } }, - // 兼容 result.data.list / result.list / result.data 为数组 等响应结构 + // 兼容 result.data.list / result.list / result.data 为数组 等响应结构(后端 CommonPage 为 result.data.list) getRawFoodList(result) { if (!result) return []; + // 若整个 result 就是列表数组(部分网关/封装可能直接返回数组) + if (Array.isArray(result)) return result; const page = result.data !== undefined && result.data !== null ? result.data : result; if (page && Array.isArray(page.list)) return page.list; - if (Array.isArray(page)) return page; + if (page && Array.isArray(page)) return page; + // 部分接口直接返回 { list: [], total: 0 } + if (result && Array.isArray(result.list)) return result.list; return []; }, getFoodImage(item) { if (!item) return this.defaultPlaceholder; const id = item.id != null ? item.id : item.foodId; if (id != null && this.imageErrorIds[String(id)]) return this.defaultPlaceholder; - // 兼容后端 image / image_url / 前端 imageUrl、img、pic、coverImage、cover_image - const raw = item.imageUrl || item.image || item.image_url || item.img || item.pic || item.coverImage || item.cover_image || ''; - const s = (raw && String(raw).trim()) || ''; + // 兼容后端 image(ToolFoodServiceImpl 返回 image)/ image_url / 前端 imageUrl、img、pic、coverImage、cover_image + const raw = item.imageUrl != null ? item.imageUrl : (item.image != null ? item.image : (item.image_url || item.img || item.pic || item.coverImage || item.cover_image || '')); + const s = (raw != null && String(raw).trim()) ? String(raw).trim() : ''; if (!s || s === 'null' || s === 'undefined') return this.defaultPlaceholder; const url = (s.startsWith('//') || s.startsWith('http')) ? s : (s.startsWith('/') ? (HTTP_REQUEST_URL || '') + s : s); return (url && String(url).trim()) ? url : this.defaultPlaceholder; }, getNutritionList(item) { if (!item) return []; + // 优先使用已规范化的 nutrition,兼容后端 nutrients / nutritions 数组 const arr = item.nutrition || item.nutrients || item.nutritions; if (Array.isArray(arr) && arr.length > 0) return arr; - // 无数组时从扁平字段组装,确保列表始终有营养简介 + // 后端列表接口(ToolFoodServiceImpl)仅返回扁平字段 energy/protein/potassium/phosphorus 等,无数组时从此组装 const list = []; const push = (label, val, unit) => { const value = (val != null && val !== '') ? String(val) + (unit || '') : '—'; @@ -322,27 +327,26 @@ export default { }; const safety = item.safety != null ? { safety: item.safety, safetyClass: item.safetyClass || 'safe' } : (safetyMap[item.suitabilityLevel] || { safety: '—', safetyClass: 'safe' }); - // 图片:兼容 image/image_url/imageUrl/img/pic/coverImage/cover_image,相对路径补全,空或无效则由 getFoodImage 用占位图 - const rawImg = item.imageUrl || item.image || item.image_url || item.img || item.pic || item.coverImage || item.cover_image || ''; - const rawStr = (rawImg && String(rawImg).trim()) || ''; + // 图片:后端列表返回 image,兼容 image_url/imageUrl/img/pic/coverImage/cover_image,相对路径补全,空或无效则由 getFoodImage 用占位图 + const rawImg = item.image || item.imageUrl || item.image_url || item.img || item.pic || item.coverImage || item.cover_image || ''; + const rawStr = (rawImg != null && String(rawImg).trim()) ? String(rawImg).trim() : ''; const validRaw = rawStr && rawStr !== 'null' && rawStr !== 'undefined'; const imageUrl = validRaw && (rawStr.startsWith('//') || rawStr.startsWith('http')) ? rawStr : (validRaw && rawStr.startsWith('/') ? (HTTP_REQUEST_URL || '') + rawStr : (validRaw ? rawStr : '')); const image = imageUrl || ''; - // 营养简介:优先 item.nutrition,其次 item.nutrients(兼容后端),否则由扁平字段 energy/protein/potassium 等组装 + // 营养简介:优先 item.nutrition,其次 item.nutrients / item.nutritions(兼容后端),否则由扁平字段 energy/protein/potassium 等组装 let nutrition = item.nutrition; + const mapNut = (n) => ({ + label: n.label || n.name || n.labelName || n.nutrientName || '—', + value: n.value != null ? String(n.value) : (n.amount != null ? String(n.amount) + (n.unit || '') : '—'), + colorClass: n.colorClass || 'green' + }); if (Array.isArray(nutrition) && nutrition.length > 0) { - nutrition = nutrition.map(n => ({ - label: n.label || n.name || n.labelName || '—', - value: n.value != null ? String(n.value) : '—', - colorClass: n.colorClass || 'green' - })); + nutrition = nutrition.map(mapNut); } else if (Array.isArray(item.nutrients) && item.nutrients.length > 0) { - nutrition = item.nutrients.map(n => ({ - label: n.label || n.name || n.labelName || '—', - value: n.value != null ? String(n.value) : '—', - colorClass: n.colorClass || 'green' - })); + nutrition = item.nutrients.map(mapNut); + } else if (Array.isArray(item.nutritions) && item.nutritions.length > 0) { + nutrition = item.nutritions.map(mapNut); } else { // 后端列表仅返回扁平字段,无 nutrition/nutrients 数组,此处组装并始终展示主要项(空值显示 —) nutrition = []; @@ -363,11 +367,12 @@ export default { const numericId = (rawId !== undefined && rawId !== null && rawId !== '' && !isNaN(Number(rawId))) ? (typeof rawId === 'number' ? rawId : Number(rawId)) : undefined; + // 保证列表项必有 image/imageUrl(空时由 getFoodImage 用 defaultPlaceholder)和 nutrition 数组(.nutrition-item 数据来源) return { ...item, id: numericId, - image, - imageUrl: image || undefined, + image: image || '', + imageUrl: image || '', category: item.category || '', safety: safety.safety, safetyClass: safety.safetyClass, diff --git a/msh_single_uniapp/pages/tool/nutrition-knowledge.vue b/msh_single_uniapp/pages/tool/nutrition-knowledge.vue index 8a19501..141fb1f 100644 --- a/msh_single_uniapp/pages/tool/nutrition-knowledge.vue +++ b/msh_single_uniapp/pages/tool/nutrition-knowledge.vue @@ -65,7 +65,7 @@ class="knowledge-item" v-for="(item, index) in (guideList || [])" :key="item.knowledgeId || item.id || index" - @click="goToDetail" :data-item-id="item.id" :data-item-kid="item.knowledgeId" + @click="goToDetail($event, item, index, 'guide')" > @@ -96,7 +96,7 @@ class="knowledge-item" v-for="(item, index) in (articleList || [])" :key="item.knowledgeId || item.id || index" - @click="goToDetail" :data-item-id="item.id" :data-item-kid="item.knowledgeId" + @click="goToDetail($event, item, index, 'articles')" > @@ -125,6 +125,7 @@