Merge branch 'release/test' into dev
This commit is contained in:
@@ -6,6 +6,8 @@ VITE_H5_OVERSEAS_API_BASE_URL=/overseas-api
|
||||
# H5 开发代理上游;正式 App 构建请在本机环境文件中改为可访问的绝对 API 根地址。
|
||||
API_PROXY_TARGET=http://127.0.0.1:9000
|
||||
OVERSEAS_API_PROXY_TARGET=http://127.0.0.1:9300
|
||||
# 上游本身以 /overseas-api 对外代理时设为 true;直连 gateway-overseas 时保持 false。
|
||||
OVERSEAS_API_PROXY_PRESERVE_PREFIX=false
|
||||
# 海外 PDA 独立更新清单地址;不得与国内 PDA 发布记录混用。
|
||||
VITE_PDA_UPDATE_URL=https://updates.example.com/pda/overseas/latest
|
||||
# 仅开发服务器自动预填;请在不提交的 .env.local 中填写真实开发账号。
|
||||
|
||||
@@ -57,16 +57,18 @@ Android App 启动后会读取 `VITE_PDA_UPDATE_URL` 并检查海外 PDA 的独
|
||||
```json
|
||||
{
|
||||
"update": true,
|
||||
"appid": "<海外PDA AppID>",
|
||||
"packageType": "wgt",
|
||||
"versionName": "0.0.2",
|
||||
"versionCode": 2,
|
||||
"minAppVersionCode": 1,
|
||||
"downloadUrl": "https://updates.example.com/pda/overseas/0.0.2.wgt",
|
||||
"mandatory": true,
|
||||
"releaseNotes": "本次更新内容"
|
||||
}
|
||||
```
|
||||
|
||||
`packageType` 只允许 `apk` 或 `wgt`。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求:
|
||||
`packageType` 只允许 `apk` 或 `wgt`,`appid` 必须与当前安装包一致。WGT 清单还须提供 `minAppVersionCode`(最低兼容 APK 整数版本),不满足时客户端拒绝安装并提示升级 APK。`mandatory=true` 时员工不能跳过,并在下载或安装失败后提供重试;wgt 安装完成会立即重启,APK 交由 Android 安装器完成覆盖安装。正式发布要求:
|
||||
|
||||
1. 海外 PDA 每次整包发布同时递增 `src/manifest.json` 的 `versionName` 与整数 `versionCode`,使用海外独立 AppID、包名和签名打包 APK。
|
||||
2. wgt 发布至少递增 `versionName`,并使用与已安装原生包兼容的 uni-app 编译器生成;新增原生模块、权限或不兼容运行时能力时必须发布 APK,不能只发 wgt。
|
||||
|
||||
+3
-1
@@ -174,7 +174,9 @@ export async function overseasDownload(path, options = {}) {
|
||||
throw errorFrom({ ...response, data: {} }, '签收照片下载失败')
|
||||
}
|
||||
const contentType = String(response?.header?.['content-type'] || response?.header?.['Content-Type'] || '').toLowerCase()
|
||||
if (expectedContentTypePrefix && !contentType.startsWith(expectedContentTypePrefix.toLowerCase())) {
|
||||
// App-Plus downloadFile may omit response headers even when the image body was downloaded successfully.
|
||||
// Reject an explicit non-image response, but allow a valid temp file when Content-Type is unavailable.
|
||||
if (expectedContentTypePrefix && contentType && !contentType.startsWith(expectedContentTypePrefix.toLowerCase())) {
|
||||
throw errorFrom({ ...response, data: {} }, '照片服务返回了无效内容,请稍后重试')
|
||||
}
|
||||
return response.tempFilePath
|
||||
|
||||
@@ -26,10 +26,11 @@ function getRuntimeInfo() {
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeUpdate(payload) {
|
||||
function normalizeUpdate(payload, runtime) {
|
||||
const value = payload && typeof payload === 'object' ? payload : {}
|
||||
if (value.update !== true) return null
|
||||
|
||||
if (value.appid !== runtime.appid) throw new Error('更新包与当前应用不匹配')
|
||||
const packageType = String(value.packageType || '').toLowerCase()
|
||||
const downloadUrl = String(value.downloadUrl || '').trim()
|
||||
const versionName = String(value.versionName || '').trim()
|
||||
@@ -37,12 +38,18 @@ function normalizeUpdate(payload) {
|
||||
throw new Error('更新服务返回了无效的版本信息')
|
||||
}
|
||||
if (!/^https:\/\//i.test(downloadUrl)) throw new Error('更新包必须使用 HTTPS 地址')
|
||||
const minAppVersionCode = Number(value.minAppVersionCode)
|
||||
if (packageType === 'wgt' && (!Number.isInteger(minAppVersionCode) || minAppVersionCode < 1
|
||||
|| minAppVersionCode > runtime.appVersionCode)) {
|
||||
throw new Error('当前安装包不支持此热更新,请安装新版 APK')
|
||||
}
|
||||
|
||||
return {
|
||||
packageType,
|
||||
downloadUrl,
|
||||
versionName,
|
||||
versionCode: Number(value.versionCode || 0),
|
||||
minAppVersionCode,
|
||||
mandatory: value.mandatory === true,
|
||||
releaseNotes: String(value.releaseNotes || '').trim()
|
||||
}
|
||||
@@ -177,7 +184,7 @@ export async function checkForAppUpdate({ manual = false } = {}) {
|
||||
})
|
||||
if (response.statusCode !== 200) throw new Error('更新服务暂时不可用')
|
||||
|
||||
const update = normalizeUpdate(response.data?.data ?? response.data)
|
||||
const update = normalizeUpdate(response.data?.data ?? response.data, runtime)
|
||||
if (update && isNewerUpdate(update, runtime)) {
|
||||
await runUpdate(update)
|
||||
} else if (manual) {
|
||||
|
||||
@@ -4,6 +4,7 @@ export const AUDIO_FEEDBACK_SOURCES = Object.freeze({
|
||||
})
|
||||
|
||||
let audioContext = null
|
||||
let hasStartedPlayback = false
|
||||
|
||||
function getAudioContext() {
|
||||
if (audioContext) return audioContext
|
||||
@@ -22,9 +23,10 @@ export function playFeedbackSound(type) {
|
||||
if (!source || !player) return false
|
||||
|
||||
try {
|
||||
player.stop()
|
||||
if (hasStartedPlayback) player.stop()
|
||||
player.src = source
|
||||
player.play()
|
||||
hasStartedPlayback = true
|
||||
return true
|
||||
} catch (_) {
|
||||
return false
|
||||
@@ -48,4 +50,5 @@ export function destroyFeedbackSound() {
|
||||
if (!audioContext) return
|
||||
try { audioContext.destroy() } catch (_) {}
|
||||
audioContext = null
|
||||
hasStartedPlayback = false
|
||||
}
|
||||
|
||||
@@ -31,6 +31,19 @@ test('overseas online update supports mandatory APK and wgt installation flows',
|
||||
assert.match(updater, /if \(retry\) await runUpdate\(update\)/)
|
||||
})
|
||||
|
||||
test('overseas update manifest rejects a domestic AppID and incompatible WGT base', async () => {
|
||||
const updater = await read('../src/utils/app-update.js')
|
||||
const body = updater.match(/function normalizeUpdate\(payload, runtime\) \{[\s\S]*?\n\}/)?.[0]
|
||||
assert.ok(body)
|
||||
const normalize = new Function(`${body}; return normalizeUpdate`)()
|
||||
const runtime = { appid: 'overseas-app', appVersionCode: 3 }
|
||||
const manifest = { update: true, appid: 'overseas-app', packageType: 'wgt', downloadUrl: 'https://example.com/app.wgt', versionName: '1.0.4', minAppVersionCode: 3 }
|
||||
assert.equal(normalize(manifest, runtime).minAppVersionCode, 3)
|
||||
assert.throws(() => normalize({ ...manifest, appid: 'domestic-app' }, runtime), /不匹配/)
|
||||
assert.throws(() => normalize({ ...manifest, minAppVersionCode: 4 }, runtime), /新版 APK/)
|
||||
assert.throws(() => normalize({ ...manifest, minAppVersionCode: undefined }, runtime), /新版 APK/)
|
||||
})
|
||||
|
||||
test('overseas profile exposes manual App update checking and the runtime version', async () => {
|
||||
const profile = await read('../src/pages/profile/profile.vue')
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ test('shared audio feedback reuses one player and switches success and error sou
|
||||
assert.equal(audio.playErrorSound(), true)
|
||||
assert.equal(created, 1)
|
||||
assert.deepEqual(calls, [
|
||||
'stop', 'play:/static/mp3/success.mp3',
|
||||
'play:/static/mp3/success.mp3',
|
||||
'stop', 'play:/static/mp3/fail.mp3'
|
||||
])
|
||||
audio.destroyFeedbackSound()
|
||||
|
||||
@@ -274,6 +274,20 @@ test('authenticated photo download maps the public api path through the H5 overs
|
||||
assert.equal(calls[0].header.Authorization, `Bearer ${value.overseasAccessToken}`)
|
||||
})
|
||||
|
||||
test('native image download accepts a valid temp file when App-Plus omits response headers', async () => {
|
||||
prepareSession()
|
||||
globalThis.uni.downloadFile = (options) => options.success({
|
||||
statusCode: 200,
|
||||
tempFilePath: '_doc/uniapp_temp/photo.jpg',
|
||||
header: {}
|
||||
})
|
||||
|
||||
assert.equal(
|
||||
await overseasDownload('/api/pda/v1/last-leg/photo', { expectedContentTypePrefix: 'image/' }),
|
||||
'_doc/uniapp_temp/photo.jpg'
|
||||
)
|
||||
})
|
||||
|
||||
test('image download rejects a JSON error envelope returned with HTTP 200', async () => {
|
||||
prepareSession()
|
||||
globalThis.uni.downloadFile = (options) => options.success({
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const domesticTarget = env.API_PROXY_TARGET || 'http://127.0.0.1:9000'
|
||||
const overseasTarget = env.OVERSEAS_API_PROXY_TARGET || 'http://127.0.0.1:9300'
|
||||
const preserveOverseasPrefix = env.OVERSEAS_API_PROXY_PRESERVE_PREFIX === 'true'
|
||||
return {
|
||||
plugins: [uni()],
|
||||
define: {
|
||||
@@ -41,7 +42,7 @@ export default defineConfig(({ mode }) => {
|
||||
'/overseas-api': loggedProxy('overseas', {
|
||||
target: overseasTarget,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/overseas-api/, '/api')
|
||||
rewrite: (path) => preserveOverseasPrefix ? path : path.replace(/^\/overseas-api/, '/api')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user