diff --git a/mobile-uni/.env b/mobile-uni/.env new file mode 100644 index 00000000..83dab315 --- /dev/null +++ b/mobile-uni/.env @@ -0,0 +1,2 @@ +VITE_APP_TERMS_URL=https://peilian.njzhmj.top/legal/terms.html +VITE_APP_PRIVACY_URL=https://peilian.njzhmj.top/legal/privacy.html diff --git a/mobile-uni/package-lock.json b/mobile-uni/package-lock.json index 30595a66..7dba8836 100644 --- a/mobile-uni/package-lock.json +++ b/mobile-uni/package-lock.json @@ -1,12 +1,12 @@ { "name": "aihr-mobile-uni", - "version": "0.1.11", + "version": "0.1.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aihr-mobile-uni", - "version": "0.1.11", + "version": "0.1.12", "dependencies": { "@dcloudio/uni-app": "3.0.0-alpha-5020220260725001", "@dcloudio/uni-app-plus": "3.0.0-alpha-5020220260725001", diff --git a/mobile-uni/package.json b/mobile-uni/package.json index e8564770..9559e05f 100644 --- a/mobile-uni/package.json +++ b/mobile-uni/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "aihr-mobile-uni", - "version": "0.1.11", + "version": "0.1.12", "private": true, "type": "module", "scripts": { diff --git a/mobile-uni/scripts/configure-android-privacy.mjs b/mobile-uni/scripts/configure-android-privacy.mjs index ba6c853c..0122ef09 100644 --- a/mobile-uni/scripts/configure-android-privacy.mjs +++ b/mobile-uni/scripts/configure-android-privacy.mjs @@ -1,5 +1,6 @@ import { readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; +import { loadEnv } from 'vite'; import { renderAndroidPrivacy } from './app-legal-config.mjs'; const projectRoot = resolve(import.meta.dirname, '..'); @@ -13,8 +14,13 @@ const argumentValue = (name) => { return process.argv.find((argument) => argument.startsWith(prefix))?.slice(prefix.length); }; -const termsUrl = argumentValue('terms-url') || process.env.VITE_APP_TERMS_URL; -const privacyUrl = argumentValue('privacy-url') || process.env.VITE_APP_PRIVACY_URL; +const publicBuildEnv = loadEnv(process.env.NODE_ENV || 'production', projectRoot, 'VITE_'); +const termsUrl = argumentValue('terms-url') + || process.env.VITE_APP_TERMS_URL + || publicBuildEnv.VITE_APP_TERMS_URL; +const privacyUrl = argumentValue('privacy-url') + || process.env.VITE_APP_PRIVACY_URL + || publicBuildEnv.VITE_APP_PRIVACY_URL; try { const rendered = renderAndroidPrivacy( diff --git a/mobile-uni/scripts/verify-android-apk.ps1 b/mobile-uni/scripts/verify-android-apk.ps1 index 129445b4..36648753 100644 --- a/mobile-uni/scripts/verify-android-apk.ps1 +++ b/mobile-uni/scripts/verify-android-apk.ps1 @@ -13,11 +13,38 @@ $ErrorActionPreference = 'Stop' $scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path $projectRoot = Split-Path -Parent $scriptDirectory +function Get-ProjectEnvValue { + param([string]$Name) + + $publicEnvPath = Join-Path $projectRoot '.env' + if (-not (Test-Path -LiteralPath $publicEnvPath -PathType Leaf)) { + return '' + } + $prefix = "$Name=" + foreach ($line in Get-Content -LiteralPath $publicEnvPath) { + $trimmed = $line.Trim() + if ($trimmed.StartsWith($prefix, [StringComparison]::Ordinal)) { + return $trimmed.Substring($prefix.Length).Trim().Trim('"').Trim("'") + } + } + return '' +} + if ([string]::IsNullOrWhiteSpace($ExpectedTermsUrl)) { - $ExpectedTermsUrl = $env:VITE_APP_TERMS_URL + $ExpectedTermsUrl = if (-not [string]::IsNullOrWhiteSpace($env:VITE_APP_TERMS_URL)) { + $env:VITE_APP_TERMS_URL + } + else { + Get-ProjectEnvValue -Name 'VITE_APP_TERMS_URL' + } } if ([string]::IsNullOrWhiteSpace($ExpectedPrivacyUrl)) { - $ExpectedPrivacyUrl = $env:VITE_APP_PRIVACY_URL + $ExpectedPrivacyUrl = if (-not [string]::IsNullOrWhiteSpace($env:VITE_APP_PRIVACY_URL)) { + $env:VITE_APP_PRIVACY_URL + } + else { + Get-ProjectEnvValue -Name 'VITE_APP_PRIVACY_URL' + } } if ( [string]::IsNullOrWhiteSpace($ExpectedTermsUrl) -xor @@ -25,6 +52,17 @@ if ( ) { throw 'ExpectedTermsUrl and ExpectedPrivacyUrl must be provided together.' } +$uncheckedConsentMessage = -join @( + [char]0x8BF7, + [char]0x5148, + [char]0x9605, + [char]0x8BFB, + [char]0x5E76, + [char]0x52FE, + [char]0x9009, + [char]0x540C, + [char]0x610F +) if ([string]::IsNullOrWhiteSpace($ApkPath)) { $ApkPath = Join-Path $projectRoot 'dist\debug\android_debug.apk' @@ -163,14 +201,20 @@ try { if ($legalAppServiceFiles.Count -ne 1) { throw "Expected exactly one App service for login legal-link verification, found $($legalAppServiceFiles.Count)." } - $legalAppService = Get-Content -Raw -Encoding UTF8 -LiteralPath $legalAppServiceFiles[0] + $legalAppServicePath = if ($legalAppServiceFiles[0] -is [IO.FileInfo]) { + $legalAppServiceFiles[0].FullName + } + else { + [string]$legalAppServiceFiles[0] + } + $legalAppService = Get-Content -Raw -Encoding UTF8 -LiteralPath $legalAppServicePath if (-not $legalAppService.Contains($ExpectedTermsUrl)) { throw 'Packaged login flow is missing the expected service agreement URL.' } if (-not $legalAppService.Contains($ExpectedPrivacyUrl)) { throw 'Packaged login flow is missing the expected privacy policy URL.' } - if (-not $legalAppService.Contains('请先阅读并勾选同意')) { + if (-not $legalAppService.Contains($uncheckedConsentMessage)) { throw 'Packaged login flow is missing the unchecked-consent failure message.' } } diff --git a/mobile-uni/scripts/verify-app-assets.mjs b/mobile-uni/scripts/verify-app-assets.mjs index d29615fb..653bd66d 100644 --- a/mobile-uni/scripts/verify-app-assets.mjs +++ b/mobile-uni/scripts/verify-app-assets.mjs @@ -1,5 +1,6 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { relative, resolve } from 'node:path'; +import { loadEnv } from 'vite'; import { validateAndroidPrivacyDocument, validateLegalUrlPair @@ -12,6 +13,7 @@ const requireLegalConsent = process.argv.includes('--require-legal-consent'); const checkLegalUrls = process.argv.includes('--check-legal-urls'); const failures = []; let releaseLegalUrls; +const publicBuildEnv = loadEnv(process.env.NODE_ENV || 'production', projectRoot, 'VITE_'); const fail = (message) => failures.push(message); const compiledAppRoot = resolve(projectRoot, 'dist/build/app'); @@ -260,8 +262,8 @@ if (requireLegalConsent) { } try { releaseLegalUrls = validateLegalUrlPair( - process.env.VITE_APP_TERMS_URL, - process.env.VITE_APP_PRIVACY_URL + process.env.VITE_APP_TERMS_URL || publicBuildEnv.VITE_APP_TERMS_URL, + process.env.VITE_APP_PRIVACY_URL || publicBuildEnv.VITE_APP_PRIVACY_URL ); } catch (error) { fail(`release build legal URL environment is invalid: ${error instanceof Error ? error.message : String(error)}`); diff --git a/mobile-uni/src/manifest.json b/mobile-uni/src/manifest.json index 3745199a..7d217e98 100644 --- a/mobile-uni/src/manifest.json +++ b/mobile-uni/src/manifest.json @@ -3,8 +3,8 @@ "appid" : "__UNI__B36D8BE", "description" : "帮道员工端", "vueVersion" : "3", - "versionName" : "0.1.11", - "versionCode" : "111", + "versionName" : "0.1.12", + "versionCode" : "112", "uniStatistics" : { "enable" : false }, diff --git a/mobile-uni/tests/app-packaging.test.mjs b/mobile-uni/tests/app-packaging.test.mjs index 297ea280..0bb81af5 100644 --- a/mobile-uni/tests/app-packaging.test.mjs +++ b/mobile-uni/tests/app-packaging.test.mjs @@ -137,6 +137,20 @@ test('正式法务地址必须使用不同的公网 HTTPS 页面', () => { )); }); +test('登录页公网法务地址由版本化公共环境配置供 npm 与 HBuilderX 共用', async () => { + const [publicEnv, configureScript, verifier] = await Promise.all([ + readFile(new URL('../.env', import.meta.url), 'utf8'), + readFile(new URL('../scripts/configure-android-privacy.mjs', import.meta.url), 'utf8'), + readFile(new URL('../scripts/verify-app-assets.mjs', import.meta.url), 'utf8') + ]); + + assert.match(publicEnv, /^VITE_APP_TERMS_URL=https:\/\/peilian\.njzhmj\.top\/legal\/terms\.html$/m); + assert.match(publicEnv, /^VITE_APP_PRIVACY_URL=https:\/\/peilian\.njzhmj\.top\/legal\/privacy\.html$/m); + assert.doesNotMatch(publicEnv, /(?:TOKEN|SECRET|PASSWORD|KEY)=/i); + assert.match(configureScript, /loadEnv\(process\.env\.NODE_ENV \|\| 'production', projectRoot, 'VITE_'\)/); + assert.match(verifier, /loadEnv\(process\.env\.NODE_ENV \|\| 'production', projectRoot, 'VITE_'\)/); +}); + test('Android 原生隐私配置明确关闭弹窗,法务地址仍须通过登录页构建校验', async () => { const template = await readFile(new URL('../androidPrivacy.json.example', import.meta.url), 'utf8'); const rendered = renderAndroidPrivacy( @@ -201,6 +215,8 @@ test('APK 门禁区分自定义调试基座与内置业务资源的测试包', a assert.match(verifier, /Packaged login flow is missing the expected service agreement URL/); assert.match(verifier, /Packaged login flow is missing the expected privacy policy URL/); assert.match(verifier, /Packaged login flow is missing the unchecked-consent failure message/); + assert.match(verifier, /\$legalAppServiceFiles\[0\]\.FullName/); + assert.doesNotMatch(verifier, /[^\x00-\x7F]/, 'Windows PowerShell 5.1 脚本必须保持纯 ASCII'); assert.match(verifier, /dist\\build\\app\\app-service\.js/); assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/); assert.match(verifier, /fixed test SMS code/);