fix: randomize eligible card photos on manual regeneration
This commit is contained in:
parent
f82dab2fc8
commit
2d77142995
@ -31,7 +31,10 @@ export default function sharingPage(kind) {
|
|||||||
if(this._sharingDisposed)return
|
if(this._sharingDisposed)return
|
||||||
const profile=displayProfile(data)
|
const profile=displayProfile(data)
|
||||||
this.setPosterProgress(15,kind==='personal'?'正在选择适合的生活照':'正在准备专属邀请')
|
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)
|
const cache=force?null:await readSharingCache(profile.id,kind,profile)
|
||||||
if(this._sharingDisposed)return
|
if(this._sharingDisposed)return
|
||||||
this.profile=profile;this.profileReady=true
|
this.profile=profile;this.profileReady=true
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
<view v-if="generating || (profileLoading && !poster)" class="asset-status card-status">{{profileLoading ? '正在读取个人资料' : posterStage}}</view>
|
<view v-if="generating || (profileLoading && !poster)" class="asset-status card-status">{{profileLoading ? '正在读取个人资料' : posterStage}}</view>
|
||||||
<view v-else-if="posterError || profileError" class="asset-error card-status"><text>{{posterError || profileError}}</text><button class="text-button" @tap="refreshPoster">重新生成</button></view>
|
<view v-else-if="posterError || profileError" class="asset-error card-status"><text>{{posterError || profileError}}</text><button class="text-button" @tap="refreshPoster">重新生成</button></view>
|
||||||
<view v-else-if="cacheNote" class="asset-status ready-status card-status">{{cacheNote}} · 点击查看大图</view>
|
<view v-else-if="cacheNote" class="asset-status ready-status card-status">{{cacheNote}} · 点击查看大图</view>
|
||||||
<view class="card-tip">资料变更后,点击“更新海报”即可重新生成。</view>
|
<view class="card-tip">点击“更新海报”,随机换一张合适的生活照。</view>
|
||||||
<view class="card-dock"><button class="secondary-button" :disabled="!poster || saving" @tap="savePoster">{{saving ? '正在保存…' : '保存海报'}}</button><button class="primary-button" open-type="share" :disabled="!profile.id">分享给好友</button></view>
|
<view class="card-dock"><button class="secondary-button" :disabled="!poster || saving" @tap="savePoster">{{saving ? '正在保存…' : '保存海报'}}</button><button class="primary-button" open-type="share" :disabled="!profile.id">分享给好友</button></view>
|
||||||
<canvas type="2d" id="fulianPoster" class="drawing-canvas"></canvas><canvas type="2d" id="fulianCover" class="drawing-canvas"></canvas>
|
<canvas type="2d" id="fulianPoster" class="drawing-canvas"></canvas><canvas type="2d" id="fulianCover" class="drawing-canvas"></canvas>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@ -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)
|
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 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
|
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 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])
|
const discarded=expired.map(k=>index[k]);expired.forEach(k=>delete index[k])
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export function photoScore(width,height) {
|
|||||||
const resolution=Math.min(1,Math.min(width/686,height/470))
|
const resolution=Math.min(1,Math.min(width/686,height/470))
|
||||||
return crop*0.8+resolution*0.2
|
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 candidates=[...new Set((photos||[]).filter(url=>typeof url==='string'&&url.trim()))].slice(0,9)
|
||||||
const checked=await Promise.all(candidates.map(async url=>{
|
const checked=await Promise.all(candidates.map(async url=>{
|
||||||
try{
|
try{
|
||||||
@ -18,5 +18,13 @@ export async function selectSharingPhoto(photos) {
|
|||||||
return {url,score:photoScore(info.width,info.height)}
|
return {url,score:photoScore(info.width,info.height)}
|
||||||
}catch(_){return {url,score:-1}}
|
}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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,17 @@ export function sharingRequest(url, method = 'GET', data = {}) {
|
|||||||
const body = response.data
|
const body = response.data
|
||||||
if (response.statusCode === 200 && body && body.code === 0 && body.data != null) return resolve(body.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 (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('暂时无法获取分享信息,请稍后重试'))
|
reject(new Error('暂时无法获取分享信息,请稍后重试'))
|
||||||
},
|
},
|
||||||
fail() { reject(new Error('网络连接失败,请检查网络后重试')) }
|
fail() { reject(new Error('网络连接失败,请检查网络后重试')) }
|
||||||
|
|||||||
@ -75,3 +75,12 @@ Node v24.19.0 六组回归均通过:check-image-path、check、check-cache、c
|
|||||||
开发者工具启动过程:自动化 SDK 在 Node24 启动 bat 失败,改用官方 CLI;游客 AppID 不被当前工具接受,改用仓库已有 AppID,仍仅加载独立禁网探针。探针分包注册失败后改成普通页面入口完成验证。测试后关闭探针项目。未上传、未部署、未更新 main 验收包;改动保留当前 feature,包含此前邀请列表详情入口。
|
开发者工具启动过程:自动化 SDK 在 Node24 启动 bat 失败,改用官方 CLI;游客 AppID 不被当前工具接受,改用仓库已有 AppID,仍仅加载独立禁网探针。探针分包注册失败后改成普通页面入口完成验证。测试后关闭探针项目。未上传、未部署、未更新 main 验收包;改动保留当前 feature,包含此前邀请列表详情入口。
|
||||||
|
|
||||||
2026-11-02 用户确认编译本轮修复进行验收。按既有授权合并本地 main 后编译,新制品和精确提交记录见 releases/20261102-poster-fix-main。未执行上传或部署。
|
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 编译;多张合格照片排除当前项,缓存保留新选择。
|
||||||
|
|||||||
12
tools/sharing/check-code-errors.cjs
Normal file
12
tools/sharing/check-code-errors.cjs
Normal file
@ -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})
|
||||||
@ -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(['portrait','bad','small','fit','panorama']),'fit')
|
||||||
assert.equal(await selectSharingPhoto(['small','bad','panorama']),'')
|
assert.equal(await selectSharingPhoto(['small','bad','panorama']),'')
|
||||||
assert.equal(await selectSharingPhoto([]),'')
|
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')
|
const runtime=h.load('utils/sharing-runtime')
|
||||||
assert.equal(runtime.displayProfile({id:42,photo:'avatar-only'}).photo,'')
|
assert.equal(runtime.displayProfile({id:42,photo:'avatar-only'}).photo,'')
|
||||||
const empty=createHarness();empty.state.profile.photos=[]
|
const empty=createHarness();empty.state.profile.photos=[]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user