ufutx_love_mp/tools/local-acceptance/runtime.js

130 lines
12 KiB
JavaScript

'use strict'
// This file is injected ONLY into the dedicated acceptance build, before app imports.
// No HTTP client, SDK, database or production credentials are used.
const ORIGIN = 'http://127.0.0.1:18765/go/api'
const IMAGE = '/images/tabBar/my.png'
const KEY = 'love.acceptance.v1'
const clone = value => JSON.parse(JSON.stringify(value))
const ok = data => ({code: 0, message: '本地模拟数据', data})
const denied = message => ({code: 1, message: '本地模拟:' + message, data: null})
function initial() {
const user = {id: 900000001, nickname: '本地测试用户', name: '本地测试用户', photo: IMAGE,
mobile: '00000000000', sex: 1, type: 'single', rank_id: 0, is_rank: 0,
is_approved: 0, is_real_approved: 0, is_educate_approved: 0, hidden_profile: 'NONE',
create_time: '2026-10-30 00:00:00', city: '测试城市', resident_city: '测试城市',
year: '95年', birthday: '1995-01-01', stature: 170, weight: 60, degree: '待核实', industry_sub: '测试职业'}
return {user, profile: {introduction: '这是合成测试资料,可编辑保存。', ideal_mate: '仅供页面验收',
interest_hobby: '阅读', interest_label: ['阅读'], city_list_claim: '', mate_conditon: []},
complete: true, failure: false, nextId: 1002, follows: [],
moments: [{id: 1001, user_id: user.id, user, content: '本地测试动态:可查看、发布和点赞。',
images: [], photos: [], pics: [], type: 'moment', is_like: 0, like_count: 0, comment_count: 0,
create_time: '2026-10-30 00:00:00', created_at: '2026-10-30 00:00:00', topics: []}], comments: {}}
}
function createMock(storage) {
let state = storage.get(KEY) || initial()
const save = () => storage.set(KEY, clone(state))
const people = () => [2, 3, 4].map(n => Object.assign({}, state.user, {id: 900000000+n, nickname: '合成用户'+n, sex: n % 2 + 1}))
function page(rows, params) { return {data: Number(params.page || 1) > 1 ? [] : rows, total: rows.length, total_pages: 1, current_page: Number(params.page || 1), last_page: 1} }
function request(url, method, input) {
if (typeof url !== 'string' || !url.startsWith(ORIGIN + '/')) return denied('已阻断非本地接口')
const path = url.slice(ORIGIN.length).split('?')[0]
const params = Object.assign({}, input || {})
;(url.split('?')[1] || '').split('&').filter(Boolean).forEach(pair => {const i=pair.indexOf('='); params[decodeURIComponent(i<0?pair:pair.slice(0,i))]=decodeURIComponent(i<0?'':pair.slice(i+1))})
method = (method || 'GET').toUpperCase()
if (state.failure && path !== '/login') return denied('模拟请求失败;请在验收入口关闭失败场景')
let result
const profile = () => Object.assign({}, state.user, state.profile)
const detail = user => ({user, base_profile: user, other_profile: clone(state.profile), profile_photos: [IMAGE],
approve_profile: {is_approved: 0, is_real_approved: 0, is_educate_approved: 0},
rank_profile: {rank_id: 0, deadline: ''}, share_qrcode: IMAGE, self_other_info: 0, self_is_real_approved: 0,
is_follow: state.follows.includes(user.id), follow_count: state.follows.length, friend_count: 0})
if (path === '/login' && method === 'POST') result = {token: 'LOCAL-MOCK-NOT-A-REAL-TOKEN', openid: 'LOCAL-MOCK', user: state.user,
im_user: {accid: 'local-user', token: 'LOCAL-MOCK'}, adv: {id:0}, is_servicer: 0, is_moment_admin: 0,
is_base_profile: state.complete, is_compeleted_profile: state.complete}
else if (path === '/user/status') result = {is_base_profile: state.complete, is_complete_profile: state.complete, is_photo_audited: 0}
else if (path === '/user/center') result = detail(state.user)
else if (path === '/user/profile' && method === 'GET') result = profile()
else if (['/user/other/profie', '/user/register/profile', '/user/info', '/profile'].includes(path) && ['POST','PUT'].includes(method)) {
// Only editable fixture fields; never accept privilege, identity, payment or approval fields.
;['introduction','ideal_mate','interest_hobby','city_list_claim'].forEach(k => {if(typeof params[k]==='string') state.profile[k]=params[k]})
;['nickname','city','resident_city','birthday','industry_sub'].forEach(k => {if(typeof params[k]==='string') state.user[k]=params[k]})
if(path === '/user/register/profile') {if(params.code !== '000000') return denied('测试验证码为 000000'); state.complete=true}
result=profile()
} else if (path === '/register/sms/code' && method === 'POST') result={mock:true, message:'未发送短信;测试验证码 000000'}
else if (path === '/area/top/users') result=people()
else if (path === '/area/top/users/status') result={status:0}
else if (path === '/area/class') result=[]
else if (path === '/other/user/recommend') result=people()
else if (/^\/other\/user\/(rand\/)?square$/.test(path)) result=page(people(),params)
else if (/^\/other\/user\/\d+$/.test(path)) {const user=people().find(p=>p.id===Number(path.split('/').pop())); if(!user)return denied('测试用户不存在');result=detail(user)}
else if (/^\/follow\/user\/\d+$/.test(path) && method==='POST') {const id=Number(path.split('/').pop());state.follows=state.follows.includes(id)?state.follows.filter(x=>x!==id):state.follows.concat(id);result={is_follow:state.follows.includes(id)}}
else if (path === '/rank/purchase/status') result={enabled:false,products:[],single_service_enabled:false,message:'会员购买暂不可用'}
else if (path === '/rank/list') result={rank_info:{nickname:'本地测试用户',rank_id:0},ranks:{name:'合成验收会员',feature_arr:[],explain_arr:['本地合成数据;购买默认关闭,不发起真实支付。'],sub_ranks:[{id:900011,name:'合成验收月卡',month:1,price:0,discount_price:0}]},pay_type:'cash'}
else if (path === '/rank/service/list') result={rank_info:state.user,services:[]}
else if (method === 'POST' && /^\/order\/virtual\/LOCAL-ACCEPTANCE-(PENDING|FULFILLED)\/confirm$/.test(path)) result={state:path.includes('-FULFILLED/')?'FULFILLED':'PENDING'}
else if (path === '/dma/carousel/list') result=[]
else if (path === '/moment/list' || /^\/moment\/list\/other\/user\/\d+$/.test(path)) result=page(state.moments,params)
else if (path === '/moment/topic/list') result=page([],params)
else if (path === '/moment/add' && method==='POST') {
if(!String(params.content||'').trim())return denied('请输入测试动态内容')
const item={id:state.nextId++,user_id:state.user.id,user:clone(state.user),content:String(params.content),images:[],photos:[],pics:[],type:'moment',is_like:0,like_count:0,comment_count:0,topics:[],create_time:'2026-10-30 00:00:00',created_at:'2026-10-30 00:00:00'}
state.moments.unshift(item);result=item
} else if (/^\/moment\/\d+$/.test(path)) {result=state.moments.find(m=>m.id===Number(path.split('/')[2]));if(!result)return denied('测试动态不存在')}
else if (/^\/moment\/\d+\/like$/.test(path) && method==='POST') {const item=state.moments.find(m=>m.id===Number(path.split('/')[2]));if(!item)return denied('测试动态不存在');item.is_like=item.is_like?0:1;item.like_count=item.is_like;result=item}
else if (/^\/moment\/\d+\/comment\/list$/.test(path)) result=params.nopage?[]:page([],params)
else if (path === '/notice/count') result={system_count:0,friend_count:0,follow_count:0,visit_count:0}
else if (['/chat/linkmen','/order/rank/list','/friends','/followers','/followings'].includes(path)) result=page([],params)
else if (/^\/label\/user\/\d+\/list$/.test(path)) result=page([],params)
else if (/^\/label\/user\/\d+\/logs$/.test(path)) result=[]
else if (path === '/interest/labels/list') result=['阅读','运动']
else if (path === '/address/country/list') result=[]
else return denied('此操作尚未模拟:'+method+' '+path+';未调用真实服务')
save();return ok(clone(result))
}
return {request, reset(){state=initial();save()}, scenario(name,value){if(name==='failure'||name==='complete'){state[name]=!!value;save()}}, snapshot(){return clone(state)}}
}
function install(wx, config = {}) {
if(config.apiOrigin && config.apiOrigin !== 'http://127.0.0.1:18766') throw new Error('Only the local acceptance gateway is permitted')
const nativeRequest=wx.request.bind(wx)
const prefix=config.apiOrigin?'acceptance-api:':'acceptance:'
const done=(options,result,success=true)=>{setTimeout(()=>{if(success){if(options.success)options.success(result)}else if(options.fail)options.fail(result);if(options.complete)options.complete(result)},0);return {abort(){},onProgressUpdate(){},offProgressUpdate(){},onHeadersReceived(){},offHeadersReceived(){}}}
const override=(name,fn)=>{wx[name]=fn;if(wx[name]!==fn)throw new Error('Cannot isolate wx.'+name+'; abort acceptance build')}
// Namespace ALL storage so an acceptance run cannot reuse or modify real login state.
const nativeGet=wx.getStorageSync.bind(wx),nativeSet=wx.setStorageSync.bind(wx),nativeRemove=wx.removeStorageSync.bind(wx)
override('getStorageSync',k=>nativeGet(prefix+k))
override('setStorageSync',(k,v)=>nativeSet(prefix+k,k==='userInfo'?Object.assign({},v,{is_base_profile:namespaced.snapshot().complete}):v))
override('removeStorageSync',k=>nativeRemove(prefix+k))
const nativeInfo=wx.getStorageInfoSync && wx.getStorageInfoSync.bind(wx)
override('clearStorageSync',()=>{if(nativeInfo)nativeInfo().keys.filter(k=>k.startsWith(prefix)).forEach(nativeRemove)})
const namespaced=createMock({get:k=>wx.getStorageSync(k),set:(k,v)=>wx.setStorageSync(k,v)})
const fixture=namespaced.snapshot()
wx.setStorageSync('userInfo',Object.assign({},fixture.user,{name:fixture.user.nickname,avatar:IMAGE,showId:fixture.user.id}))
wx.setStorageSync('adv',{id:0})
override('request',options=>{
const suffix=typeof options.url==='string' && options.url.startsWith(ORIGIN+'/')?options.url.slice(ORIGIN.length):''
const route=(options.method||'GET').toUpperCase()+' '+suffix.split('?')[0]
const realRoutes=['POST /login','GET /user/profile','PUT /user/other/profie','GET /user/status','GET /dma/carousel/list','GET /rank/purchase/status']
if(config.apiOrigin && realRoutes.includes(route) && !namespaced.snapshot().failure) {
// Exact origin and method/path allowlist; no fallback to production or silent mock success.
return nativeRequest(Object.assign({},options,{url:config.apiOrigin+'/go/api'+suffix}))
}
let response;try{response=namespaced.request(options.url,options.method,options.data)}catch(e){response=denied('模拟请求参数无效')}
return done(options,{statusCode:200,header:{},data:response,errMsg:'request:ok'})
})
override('login',options=>done(options,{code:'LOCAL-MOCK-CODE',errMsg:'login:ok'}))
const blocked=['uploadFile','downloadFile','connectSocket','requestPayment','requestVirtualPayment','requestSubscribeMessage','navigateToMiniProgram','openCustomerServiceChat','makePhoneCall','startFacialRecognitionVerify','startFacialRecognitionVerifyAndUploadVideo','openUrl','openDocument','shareAppMessage']
blocked.forEach(name=>override(name,(options={})=>{if(wx.showToast)wx.showToast({title:'本地模拟:此第三方操作已禁用',icon:'none'});return done(options,{errMsg:name+':fail local mock blocked'},false)}))
override('getFuzzyLocation',options=>done(options,{latitude:0,longitude:0,errMsg:'getFuzzyLocation:ok'}))
override('chooseLocation',options=>done(options,{latitude:0,longitude:0,name:'测试地点',address:'合成地址',errMsg:'chooseLocation:ok'}))
override('getLogManager',()=>({log(){},info(){},debug(){},warn(){}}))
override('getRealtimeLogManager',()=>({info(){},warn(){},error(){}}))
override('getUpdateManager',()=>({onCheckForUpdate(){},onUpdateReady(){},onUpdateFailed(){},applyUpdate(){}}))
// Cloud APIs must never retain access to any previously configured real environment.
if(wx.cloud) Object.keys(wx.cloud).forEach(name=>{if(typeof wx.cloud[name]==='function'){wx.cloud[name]=()=>{throw new Error('Cloud disabled in local acceptance') }}})
const nativeImage=wx.getImageInfo && wx.getImageInfo.bind(wx)
if(nativeImage)override('getImageInfo',options=>nativeImage(Object.assign({},options,{src:/^(https?:)?\/\//i.test(options.src||'')?IMAGE:options.src})))
wx.__loveAcceptance=namespaced
return namespaced
}
module.exports={ORIGIN,createMock,install}