From 2d7714299580cd2626c69636dcde5c9b2e232a04 Mon Sep 17 00:00:00 2001 From: Glore Zeng Date: Tue, 3 Nov 2026 06:06:39 +0800 Subject: [PATCH] fix: randomize eligible card photos on manual regeneration --- src/mixins/sharing-page.js | 5 ++++- src/pages/users/shareCard.wpy | 2 +- src/utils/sharing-cache.js | 2 +- src/utils/sharing-photo.js | 12 ++++++++++-- src/utils/sharing-runtime.js | 11 +++++++++++ tools/sharing/VALIDATION-20261102.md | 9 +++++++++ tools/sharing/check-code-errors.cjs | 12 ++++++++++++ tools/sharing/check-photo.cjs | 12 ++++++++++++ 8 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tools/sharing/check-code-errors.cjs diff --git a/src/mixins/sharing-page.js b/src/mixins/sharing-page.js index 6ce8561..f6423ea 100644 --- a/src/mixins/sharing-page.js +++ b/src/mixins/sharing-page.js @@ -31,7 +31,10 @@ export default function sharingPage(kind) { if(this._sharingDisposed)return const profile=displayProfile(data) this.setPosterProgress(15,kind==='personal'?'正在选择适合的生活照':'正在准备专属邀请') - if(kind==='personal')profile.photo=await selectSharingPhoto(profile.photos) + if(kind==='personal'){ + const previousCache=await readSharingCache(profile.id,kind) + profile.photo=await selectSharingPhoto(profile.photos,{random:force,previous:this.profile.photo||previousCache?.selectedPhoto,preferred:previousCache?.selectedPhoto}) + } const cache=force?null:await readSharingCache(profile.id,kind,profile) if(this._sharingDisposed)return this.profile=profile;this.profileReady=true diff --git a/src/pages/users/shareCard.wpy b/src/pages/users/shareCard.wpy index becd0dd..7d09ebd 100644 --- a/src/pages/users/shareCard.wpy +++ b/src/pages/users/shareCard.wpy @@ -14,7 +14,7 @@ {{profileLoading ? '正在读取个人资料' : posterStage}} {{posterError || profileError}} {{cacheNote}} · 点击查看大图 - 资料变更后,点击“更新海报”即可重新生成。 + 点击“更新海报”,随机换一张合适的生活照。 diff --git a/src/utils/sharing-cache.js b/src/utils/sharing-cache.js index 1d1c006..597ab88 100644 --- a/src/utils/sharing-cache.js +++ b/src/utils/sharing-cache.js @@ -43,7 +43,7 @@ export async function writeSharingCache(profile,kind,assets) { await boundedCall((resolve,reject)=>wx.getFileSystemManager().copyFile({srcPath:assets[name],destPath:paths[name],success:resolve,fail:reject}), '图片缓存失败', 3000) } const index=readIndex(),previous=index[key] - const record={...paths,version:VERSION,fingerprint:sharingFingerprint(profile,kind),auth:digest(String(wx.getStorageSync('token')||'')),savedAt:Date.now()} + const record={...paths,selectedPhoto:profile.photo||'',version:VERSION,fingerprint:sharingFingerprint(profile,kind),auth:digest(String(wx.getStorageSync('token')||'')),savedAt:Date.now()} index[key]=record const expired=Object.keys(index).filter(k=>k!==key).sort((a,b)=>(index[b].savedAt||0)-(index[a].savedAt||0)).slice(3) const discarded=expired.map(k=>index[k]);expired.forEach(k=>delete index[k]) diff --git a/src/utils/sharing-photo.js b/src/utils/sharing-photo.js index 50e4c1d..48d0b16 100644 --- a/src/utils/sharing-photo.js +++ b/src/utils/sharing-photo.js @@ -10,7 +10,7 @@ export function photoScore(width,height) { const resolution=Math.min(1,Math.min(width/686,height/470)) return crop*0.8+resolution*0.2 } -export async function selectSharingPhoto(photos) { +export async function selectSharingPhoto(photos,options={}) { const candidates=[...new Set((photos||[]).filter(url=>typeof url==='string'&&url.trim()))].slice(0,9) const checked=await Promise.all(candidates.map(async url=>{ try{ @@ -18,5 +18,13 @@ export async function selectSharingPhoto(photos) { return {url,score:photoScore(info.width,info.height)} }catch(_){return {url,score:-1}} })) - return checked.filter(item=>item.score>=0).sort((a,b)=>b.score-a.score)[0]?.url||'' + const eligible=checked.filter(item=>item.score>=0) + if(!eligible.length)return '' + if(options.random){ + const alternatives=eligible.filter(item=>item.url!==options.previous) + const pool=alternatives.length?alternatives:eligible + return pool[Math.floor(Math.random()*pool.length)].url + } + if(options.preferred&&eligible.some(item=>item.url===options.preferred))return options.preferred + return eligible.sort((a,b)=>b.score-a.score)[0].url } diff --git a/src/utils/sharing-runtime.js b/src/utils/sharing-runtime.js index 5c2c8c1..976bba9 100644 --- a/src/utils/sharing-runtime.js +++ b/src/utils/sharing-runtime.js @@ -26,6 +26,17 @@ export function sharingRequest(url, method = 'GET', data = {}) { const body = response.data if (response.statusCode === 200 && body && body.code === 0 && body.data != null) return resolve(body.data) if (response.statusCode === 401 || (body && (body.code === 2 || body.code === 3))) return reject(new Error('登录状态已失效,请重新登录后再试')) + if(url.endsWith('/sharing/code')){ + if(response.statusCode!==200)return reject(new Error(`分享码接口异常(HTTP ${Number(response.statusCode)||0}),请联系客服`)) + const messages={qr_token_refresh:'分享码凭证更新失败,请稍后重试',qr_upstream_unavailable:'分享码服务暂不可用,请稍后重试',qr_image_read:'分享码图片读取失败,请重试',qr_image_format:'分享码图片格式异常,请联系客服',qr_wechat_rejected:'微信未能生成分享码,请联系客服'} + const message=messages[body&&body.error_key] + const code=Number(body&&body.upstream_code) + if(message)return reject(new Error(message+(Number.isSafeInteger(code)&&code!==0?`(微信码 ${code})`:''))) + // Old servers do not have error_key. Only expose known safe messages. + const known=['生成分享码失败,请稍后重试','生成分享码失败','分享码格式无效','分享类型无效'] + if(body&&known.includes(body.message))return reject(new Error(body.message)) + return reject(new Error('分享码获取失败,请联系客服核查接口版本')) + } reject(new Error('暂时无法获取分享信息,请稍后重试')) }, fail() { reject(new Error('网络连接失败,请检查网络后重试')) } diff --git a/tools/sharing/VALIDATION-20261102.md b/tools/sharing/VALIDATION-20261102.md index 2d11058..30b86de 100644 --- a/tools/sharing/VALIDATION-20261102.md +++ b/tools/sharing/VALIDATION-20261102.md @@ -75,3 +75,12 @@ Node v24.19.0 六组回归均通过:check-image-path、check、check-cache、c 开发者工具启动过程:自动化 SDK 在 Node24 启动 bat 失败,改用官方 CLI;游客 AppID 不被当前工具接受,改用仓库已有 AppID,仍仅加载独立禁网探针。探针分包注册失败后改成普通页面入口完成验证。测试后关闭探针项目。未上传、未部署、未更新 main 验收包;改动保留当前 feature,包含此前邀请列表详情入口。 2026-11-02 用户确认编译本轮修复进行验收。按既有授权合并本地 main 后编译,新制品和精确提交记录见 releases/20261102-poster-fix-main。未执行上传或部署。 + +## 2026-11-03 手动更新随机换生活照 + +分支 feature/card-photo-refresh。个人卡片点击“更新海报”或失败后的“重新生成”,重新读取本人生活照并筛选既有尺寸/比例条件,在合格候选中随机选择;多张时排除上一张,单张继续使用,无合格照片返回品牌默认背景。首次进入仍按适配度优选;将 selectedPhoto 写入本地缓存,正常重进保持上次随机照片,照片删除或不再合格时重新选择。邀请海报行为不变。 + +Node v24.19.0:六组分享回归退出0。check-photo 增加随机排除当前、单张、无照片、优先缓存照片、重进不重新导出、照片删除后替换;额外执行生成失败后点击重试换图检查,退出0。git diff --check通过,后端没有本次改动,未上传发布。 +Node v16.20.2 在 .release-build/invitation-card-candidate 执行 tools/build/build.cjs,退出0,89模板编译完成。改动仍在功能分支,未更新 main 验收包。 + +2026-11-03 用户确认后端修复生效但线上换图未变化。核实上一交付前端 main f82dab2 仍固定优选,随机换图代码尚在功能分支。本轮补齐前端交付,合并后从 main 编译;多张合格照片排除当前项,缓存保留新选择。 diff --git a/tools/sharing/check-code-errors.cjs b/tools/sharing/check-code-errors.cjs new file mode 100644 index 0000000..fb58674 --- /dev/null +++ b/tools/sharing/check-code-errors.cjs @@ -0,0 +1,12 @@ +const assert=require('assert'),{createHarness}=require('./harness.cjs') +;(async()=>{ + const h=createHarness(),{sharingRequest}=h.load('utils/sharing-runtime'),url='https://synthetic.invalid/user/sharing/code' + for(const [response,pattern] of [ + [{statusCode:404,data:{}},/HTTP 404/], + [{statusCode:200,data:{code:1,error_key:'qr_wechat_rejected',upstream_code:41030,message:'secret'}},/微信码 41030/], + [{statusCode:200,data:{code:1,error_key:'qr_token_refresh'}},/凭证更新失败/], + [{statusCode:200,data:{code:1,message:'生成分享码失败,请稍后重试'}},/生成分享码失败/], + [{statusCode:200,data:{code:1,message:'SYNTHETIC_PRIVATE_SECRET'}},/核查接口版本/] + ]){h.flags.response=response;await assert.rejects(sharingRequest(url,'POST',{kind:'invite'}),e=>pattern.test(e.message)&&!e.message.includes('secret')&&!e.message.includes('PRIVATE'))} + console.log('PASS: QR HTTP/WeChat/credential/legacy failures distinguishable without leaking raw responses.') +})().catch(e=>{console.error(e.stack);process.exitCode=1}) diff --git a/tools/sharing/check-photo.cjs b/tools/sharing/check-photo.cjs index fc6dffe..e556a75 100644 --- a/tools/sharing/check-photo.cjs +++ b/tools/sharing/check-photo.cjs @@ -10,6 +10,18 @@ const assert=require('assert'),{createHarness}=require('./harness.cjs') assert.equal(await selectSharingPhoto(['portrait','bad','small','fit','panorama']),'fit') assert.equal(await selectSharingPhoto(['small','bad','panorama']),'') assert.equal(await selectSharingPhoto([]),'') + assert.equal(await selectSharingPhoto(['portrait','fit','small','bad'],{random:true,previous:'fit'}),'portrait') + assert.equal(await selectSharingPhoto(['fit','fit'],{random:true,previous:'fit'}),'fit') + assert.equal(await selectSharingPhoto(['bad','small'],{random:true}),'') + assert.equal(await selectSharingPhoto(['portrait','fit'],{preferred:'portrait'}),'portrait') + const refresh=createHarness();refresh.state.profile.photos=['first','second'] + const refreshed=refresh.page('shareCard');refreshed.hook('onReady');await refreshed.startSharing() + const initial=refreshed.profile.photo;await refreshed.refreshPoster() + assert.notEqual(refreshed.profile.photo,initial);assert(refreshed.poster) + const chosen=refreshed.profile.photo,poster=refreshed.poster,exportsBefore=refresh.state.exportID + const revisit=refresh.page('shareCard');revisit.hook('onReady');await revisit.startSharing() + assert.equal(revisit.profile.photo,chosen);assert.equal(revisit.poster,poster);assert.equal(refresh.state.exportID,exportsBefore) + refresh.state.profile.photos=[initial];await revisit.loadSharingProfile();assert.equal(revisit.profile.photo,initial) const runtime=h.load('utils/sharing-runtime') assert.equal(runtime.displayProfile({id:42,photo:'avatar-only'}).photo,'') const empty=createHarness();empty.state.profile.photos=[]