From f2dabe6b9158a2c85c7c72c66da21ba27e91fc99 Mon Sep 17 00:00:00 2001 From: "Guangfei.Zhao" Date: Wed, 2 Sep 2026 23:23:43 +0800 Subject: [PATCH 1/5] feat: Add build info and localization for entry app - Introduced buildinfo.json5 to enable impeller feature. - Added Chinese localization strings in string.json for module description and entry ability. - Created Ability.test.ets for unit testing of ability functionalities. - Implemented List.test.ets to run the ability tests. - Developed TestAbility.ets to manage test execution and lifecycle events. - Created Index.ets as a testability page with UI components. - Added OpenHarmonyTestRunner.ts for managing test execution in OpenHarmony. - Defined module.json5 for entry_test module configuration. - Established color.json and string.json for base resources. - Included icon.png for the test module. - Created test_pages.json to define testability page sources. - Configured hvigor and oh-package.json5 for project setup. - Updated pubspec.yaml and related files to downgrade Dart SDK to 3.11.0 and adjust dependencies. - Added PowerShell script to enable OpenHarmony Flutter toolchain. - Updated multiple package pubspec.yaml files to align with Dart SDK version 3.11.0 and adjust dependencies accordingly. --- app/.metadata | 11 ++-- app/ohos/.gitignore | 20 +++++++ app/ohos/AppScope/app.json5 | 10 ++++ .../resources/base/element/string.json | 8 +++ .../resources/base/media/app_icon.png | Bin 0 -> 6790 bytes app/ohos/build-profile.json5 | 39 ++++++++++++ app/ohos/entry/.gitignore | 7 +++ app/ohos/entry/build-profile.json5 | 15 +++++ app/ohos/entry/hvigorfile.ts | 7 +++ app/ohos/entry/oh-package.json5 | 11 ++++ .../main/ets/entryability/EntryAbility.ets | 10 ++++ app/ohos/entry/src/main/ets/pages/Index.ets | 24 ++++++++ app/ohos/entry/src/main/module.json5 | 40 +++++++++++++ .../main/resources/base/element/color.json | 8 +++ .../main/resources/base/element/string.json | 16 +++++ .../src/main/resources/base/media/icon.png | Bin 0 -> 6790 bytes .../resources/base/profile/main_pages.json | 5 ++ .../main/resources/en_US/element/string.json | 16 +++++ .../main/resources/rawfile/buildinfo.json5 | 8 +++ .../main/resources/zh_CN/element/string.json | 16 +++++ .../src/ohosTest/ets/test/Ability.test.ets | 35 +++++++++++ .../entry/src/ohosTest/ets/test/List.test.ets | 5 ++ .../ohosTest/ets/testability/TestAbility.ets | 48 +++++++++++++++ .../ohosTest/ets/testability/pages/Index.ets | 36 +++++++++++ .../ets/testrunner/OpenHarmonyTestRunner.ts | 50 ++++++++++++++++ app/ohos/entry/src/ohosTest/module.json5 | 37 ++++++++++++ .../resources/base/element/color.json | 8 +++ .../resources/base/element/string.json | 16 +++++ .../ohosTest/resources/base/media/icon.png | Bin 0 -> 6790 bytes .../resources/base/profile/test_pages.json | 5 ++ app/ohos/hvigor/hvigor-config.json5 | 6 ++ app/ohos/hvigorconfig.ts | 4 ++ app/ohos/hvigorfile.ts | 8 +++ app/ohos/oh-package.json5 | 13 ++++ app/pubspec.yaml | 4 +- docs/01-project-structure.md | 4 +- docs/07-native-integration.md | 2 +- enable-ohos.ps1 | 46 ++++++++++++++ packages/core_analytics/pubspec.yaml | 4 +- packages/core_auth/pubspec.yaml | 10 ++-- packages/core_foundation/pubspec.yaml | 4 +- packages/core_logging/pubspec.yaml | 4 +- packages/core_network/pubspec.yaml | 4 +- packages/core_router/pubspec.yaml | 4 +- packages/core_storage/pubspec.yaml | 4 +- packages/core_ui/pubspec.yaml | 4 +- packages/core_webview/pubspec.yaml | 4 +- packages/feature_auth/pubspec.yaml | 9 +-- packages/feature_home/pubspec.yaml | 9 +-- packages/native_scan/example/pubspec.yaml | 2 +- packages/native_scan/pubspec.yaml | 6 +- pubspec.lock | 56 +++++++++--------- pubspec.yaml | 2 +- 53 files changed, 655 insertions(+), 69 deletions(-) create mode 100644 app/ohos/.gitignore create mode 100644 app/ohos/AppScope/app.json5 create mode 100644 app/ohos/AppScope/resources/base/element/string.json create mode 100644 app/ohos/AppScope/resources/base/media/app_icon.png create mode 100644 app/ohos/build-profile.json5 create mode 100644 app/ohos/entry/.gitignore create mode 100644 app/ohos/entry/build-profile.json5 create mode 100644 app/ohos/entry/hvigorfile.ts create mode 100644 app/ohos/entry/oh-package.json5 create mode 100644 app/ohos/entry/src/main/ets/entryability/EntryAbility.ets create mode 100644 app/ohos/entry/src/main/ets/pages/Index.ets create mode 100644 app/ohos/entry/src/main/module.json5 create mode 100644 app/ohos/entry/src/main/resources/base/element/color.json create mode 100644 app/ohos/entry/src/main/resources/base/element/string.json create mode 100644 app/ohos/entry/src/main/resources/base/media/icon.png create mode 100644 app/ohos/entry/src/main/resources/base/profile/main_pages.json create mode 100644 app/ohos/entry/src/main/resources/en_US/element/string.json create mode 100644 app/ohos/entry/src/main/resources/rawfile/buildinfo.json5 create mode 100644 app/ohos/entry/src/main/resources/zh_CN/element/string.json create mode 100644 app/ohos/entry/src/ohosTest/ets/test/Ability.test.ets create mode 100644 app/ohos/entry/src/ohosTest/ets/test/List.test.ets create mode 100644 app/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets create mode 100644 app/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets create mode 100644 app/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts create mode 100644 app/ohos/entry/src/ohosTest/module.json5 create mode 100644 app/ohos/entry/src/ohosTest/resources/base/element/color.json create mode 100644 app/ohos/entry/src/ohosTest/resources/base/element/string.json create mode 100644 app/ohos/entry/src/ohosTest/resources/base/media/icon.png create mode 100644 app/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json create mode 100644 app/ohos/hvigor/hvigor-config.json5 create mode 100644 app/ohos/hvigorconfig.ts create mode 100644 app/ohos/hvigorfile.ts create mode 100644 app/ohos/oh-package.json5 create mode 100644 enable-ohos.ps1 diff --git a/app/.metadata b/app/.metadata index db0a0fe..9ac8de8 100644 --- a/app/.metadata +++ b/app/.metadata @@ -4,8 +4,8 @@ # This file should be version controlled and should not be manually edited. version: - revision: "6b182d2c7585eba26d4edce0f97630effd256c33" - channel: "stable" + revision: "eb712e92d4c63e8051990ceea01fdcf88a7afd5d" + channel: "[user-branch]" project_type: app @@ -13,14 +13,17 @@ project_type: app migration: platforms: - platform: root - create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 - base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + create_revision: eb712e92d4c63e8051990ceea01fdcf88a7afd5d + base_revision: eb712e92d4c63e8051990ceea01fdcf88a7afd5d - platform: android create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 - platform: ios create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + - platform: ohos + create_revision: eb712e92d4c63e8051990ceea01fdcf88a7afd5d + base_revision: eb712e92d4c63e8051990ceea01fdcf88a7afd5d # User provided section diff --git a/app/ohos/.gitignore b/app/ohos/.gitignore new file mode 100644 index 0000000..e326491 --- /dev/null +++ b/app/ohos/.gitignore @@ -0,0 +1,20 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +**/BuildProfile.ets +**/oh-package-lock.json5 +/package.json +/package-lock.json + +**/src/main/resources/rawfile/flutter_assets/ +**/libs/**/libapp.so +**/libs/**/libflutter.so +**/libs/**/libvmservice_snapshot.so diff --git a/app/ohos/AppScope/app.json5 b/app/ohos/AppScope/app.json5 new file mode 100644 index 0000000..bd240b6 --- /dev/null +++ b/app/ohos/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.conti.app", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/app/ohos/AppScope/resources/base/element/string.json b/app/ohos/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..198f1e7 --- /dev/null +++ b/app/ohos/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "app" + } + ] +} diff --git a/app/ohos/AppScope/resources/base/media/app_icon.png b/app/ohos/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c GIT binary patch literal 6790 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 { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', + JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); + } + + onForeground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); + } + + onBackground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); + } +} \ No newline at end of file diff --git a/app/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets b/app/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets new file mode 100644 index 0000000..545843d --- /dev/null +++ b/app/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets @@ -0,0 +1,36 @@ + + +import hilog from '@ohos.hilog'; + +@Entry +@Component +struct Index { + aboutToAppear() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility index aboutToAppear'); + } + @State message: string = 'Hello World' + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + Button() { + Text('next page') + .fontSize(20) + .fontWeight(FontWeight.Bold) + }.type(ButtonType.Capsule) + .margin({ + top: 20 + }) + .backgroundColor('#0D9FFB') + .width('35%') + .height('5%') + .onClick(()=>{ + }) + } + .width('100%') + } + .height('100%') + } + } \ No newline at end of file diff --git a/app/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts b/app/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts new file mode 100644 index 0000000..47fc89b --- /dev/null +++ b/app/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts @@ -0,0 +1,50 @@ + +import hilog from '@ohos.hilog'; +import TestRunner from '@ohos.application.testRunner'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; + +var abilityDelegator = undefined +var abilityDelegatorArguments = undefined + +async function onAbilityCreateCallback() { + hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); +} + +async function addAbilityMonitorCallback(err: any) { + hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); +} + +export default class OpenHarmonyTestRunner implements TestRunner { + constructor() { + } + + onPrepare() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); + } + + async onRun() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + var testAbilityName = abilityDelegatorArguments.bundleName + '.TestAbility' + let lMonitor = { + abilityName: testAbilityName, + onAbilityCreate: onAbilityCreateCallback, + }; + abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) + var cmd = 'aa start -d 0 -a TestAbility' + ' -b ' + abilityDelegatorArguments.bundleName + var debug = abilityDelegatorArguments.parameters['-D'] + if (debug == 'true') + { + cmd += ' -D' + } + hilog.info(0x0000, 'testTag', 'cmd : %{public}s', cmd); + abilityDelegator.executeShellCommand(cmd, + (err: any, d: any) => { + hilog.info(0x0000, 'testTag', 'executeShellCommand : err : %{public}s', JSON.stringify(err) ?? ''); + hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.stdResult ?? ''); + hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.exitCode ?? ''); + }) + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); + } +} \ No newline at end of file diff --git a/app/ohos/entry/src/ohosTest/module.json5 b/app/ohos/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000..3b02a75 --- /dev/null +++ b/app/ohos/entry/src/ohosTest/module.json5 @@ -0,0 +1,37 @@ + +{ + "module": { + "name": "entry_test", + "type": "feature", + "description": "$string:module_test_desc", + "mainElement": "TestAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:test_pages", + "abilities": [ + { + "name": "TestAbility", + "srcEntry": "./ets/testability/TestAbility.ets", + "description": "$string:TestAbility_desc", + "icon": "$media:icon", + "label": "$string:TestAbility_label", + "exported": true, + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "skills": [ + { + "actions": [ + "action.system.home" + ], + "entities": [ + "entity.system.home" + ] + } + ] + } + ] + } +} diff --git a/app/ohos/entry/src/ohosTest/resources/base/element/color.json b/app/ohos/entry/src/ohosTest/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/app/ohos/entry/src/ohosTest/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/app/ohos/entry/src/ohosTest/resources/base/element/string.json b/app/ohos/entry/src/ohosTest/resources/base/element/string.json new file mode 100644 index 0000000..65d8fa5 --- /dev/null +++ b/app/ohos/entry/src/ohosTest/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_test_desc", + "value": "test ability description" + }, + { + "name": "TestAbility_desc", + "value": "the test ability" + }, + { + "name": "TestAbility_label", + "value": "test label" + } + ] +} \ No newline at end of file diff --git a/app/ohos/entry/src/ohosTest/resources/base/media/icon.png b/app/ohos/entry/src/ohosTest/resources/base/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c GIT binary patch literal 6790 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.44.0' flutter: diff --git a/enable-ohos.ps1 b/enable-ohos.ps1 new file mode 100644 index 0000000..93b6b8f --- /dev/null +++ b/enable-ohos.ps1 @@ -0,0 +1,46 @@ +# Enables the OpenHarmony Flutter toolchain for the current PowerShell session. +# This script does not change the system or user PATH permanently. + +$ErrorActionPreference = 'Stop' + +$flutterBin = 'C:\Users\A200477427\Developments\flutter_flutter\bin' +$devecoHome = 'C:\Program Files\Huawei\DevEco Studio' +$devecoSdk = "$devecoHome\sdk" +$ohpmBin = "$devecoHome\tools\ohpm\bin" +$hvigorBin = "$devecoHome\tools\hvigor\bin" + +$requiredDirectories = @($flutterBin, $devecoSdk, $ohpmBin, $hvigorBin) +$missingDirectories = $requiredDirectories | Where-Object { + -not (Test-Path -LiteralPath $_ -PathType Container) +} + +if ($missingDirectories) { + throw "OpenHarmony tool directories were not found:`n$($missingDirectories -join "`n")" +} + +# Set these only for this terminal. This also makes the script work in a terminal +# that was opened before the corresponding Windows environment variables existed. +$env:DEVECO_HOME = $devecoHome +$env:DEVECO_SDK_HOME = $devecoSdk + +# Put the OpenHarmony-specific commands first for this terminal only. Remove +# duplicate entries so repeated execution of this script leaves PATH clean. +$pathDirectories = @($flutterBin, $ohpmBin, $hvigorBin) +$pathEntries = $env:Path -split ';' | Where-Object { + $_ -and $_ -notin $pathDirectories +} +$env:Path = ($pathDirectories + $pathEntries) -join ';' + +$commands = @('flutter', 'ohpm', 'hvigorw') +$unavailableCommands = foreach ($command in $commands) { + if (-not (Get-Command $command -ErrorAction SilentlyContinue)) { + $command + } +} + +if ($unavailableCommands) { + throw "The following commands could not be resolved from PATH: $($unavailableCommands -join ', ')" +} + +Write-Host 'OpenHarmony Flutter environment is enabled for this PowerShell session.' -ForegroundColor Green +Get-Command $commands | Select-Object Name, Source, CommandType | Format-Table -AutoSize diff --git a/packages/core_analytics/pubspec.yaml b/packages/core_analytics/pubspec.yaml index 04e8268..ce6f72a 100644 --- a/packages/core_analytics/pubspec.yaml +++ b/packages/core_analytics/pubspec.yaml @@ -5,13 +5,13 @@ version: 0.1.0 resolution: workspace environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_foundation: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 dev_dependencies: flutter_lints: ^6.0.0 diff --git a/packages/core_auth/pubspec.yaml b/packages/core_auth/pubspec.yaml index 822d321..00339ed 100644 --- a/packages/core_auth/pubspec.yaml +++ b/packages/core_auth/pubspec.yaml @@ -11,16 +11,18 @@ resolution: workspace # 因此本包里的 token 刷新用的是一个**不带任何拦截器的裸 Dio**。 # --------------------------------------------------------------------------- environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_foundation: ^0.1.0 dio: ^5.11.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 flutter_secure_storage: ^11.0.0 - riverpod_annotation: ^4.0.3 + # 4.0.3+ 的 generator 引入 analyzer ^12,要求 meta ^1.18; + # Harmony Flutter(Dart 3.11)固定 meta 1.17.0,故保持在兼容组合。 + riverpod_annotation: 4.0.2 dev_dependencies: build_runner: ^2.4.13 @@ -28,4 +30,4 @@ dev_dependencies: flutter_test: sdk: flutter mocktail: ^1.0.5 - riverpod_generator: ^4.0.4 + riverpod_generator: 4.0.3 diff --git a/packages/core_foundation/pubspec.yaml b/packages/core_foundation/pubspec.yaml index 7f1e5ae..ae75a94 100644 --- a/packages/core_foundation/pubspec.yaml +++ b/packages/core_foundation/pubspec.yaml @@ -5,12 +5,12 @@ version: 0.1.0 resolution: workspace environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 dev_dependencies: flutter_lints: ^6.0.0 diff --git a/packages/core_logging/pubspec.yaml b/packages/core_logging/pubspec.yaml index 77d8cbc..c60bd1c 100644 --- a/packages/core_logging/pubspec.yaml +++ b/packages/core_logging/pubspec.yaml @@ -5,13 +5,13 @@ version: 0.1.0 resolution: workspace environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_foundation: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 logger: ^2.7.0 sentry_flutter: ^9.26.0 diff --git a/packages/core_network/pubspec.yaml b/packages/core_network/pubspec.yaml index 53d165e..4594546 100644 --- a/packages/core_network/pubspec.yaml +++ b/packages/core_network/pubspec.yaml @@ -5,7 +5,7 @@ version: 0.1.0 resolution: workspace environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_auth: ^0.1.0 @@ -13,7 +13,7 @@ dependencies: dio: ^5.11.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 path: ^1.9.1 uuid: ^4.5.1 diff --git a/packages/core_router/pubspec.yaml b/packages/core_router/pubspec.yaml index 2b0a37a..043bb30 100644 --- a/packages/core_router/pubspec.yaml +++ b/packages/core_router/pubspec.yaml @@ -8,14 +8,14 @@ resolution: workspace # 注意:本包**不**依赖任何 feature_*。feature 路由由 app/ 通过 appRoutesProvider 注入, # 详见 lib/src/app_router.dart 顶部注释与 SCAFFOLD-NOTES.md §G。 environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_auth: ^0.1.0 core_foundation: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 go_router: ^17.5.0 dev_dependencies: diff --git a/packages/core_storage/pubspec.yaml b/packages/core_storage/pubspec.yaml index 130e851..11a1350 100644 --- a/packages/core_storage/pubspec.yaml +++ b/packages/core_storage/pubspec.yaml @@ -12,13 +12,13 @@ resolution: workspace # 连带收益:去掉 drift_dev 后 melos 可以留在 8.x(drift_dev 2.34.0 依赖 # cli_util ^0.4.0,与 melos 8 的 ^0.5.0 互斥)。详见 SCAFFOLD-NOTES.md §A。 environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_foundation: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 shared_preferences: ^2.5.5 dev_dependencies: diff --git a/packages/core_ui/pubspec.yaml b/packages/core_ui/pubspec.yaml index aaa88ac..d349ee6 100644 --- a/packages/core_ui/pubspec.yaml +++ b/packages/core_ui/pubspec.yaml @@ -5,13 +5,13 @@ version: 0.1.0 resolution: workspace environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_foundation: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 dev_dependencies: flutter_lints: ^6.0.0 diff --git a/packages/core_webview/pubspec.yaml b/packages/core_webview/pubspec.yaml index 0580b78..57e2c7a 100644 --- a/packages/core_webview/pubspec.yaml +++ b/packages/core_webview/pubspec.yaml @@ -8,14 +8,14 @@ resolution: workspace # 「用 target 换真实 URL」的接口调用**不在本包**——那需要 core_network,不是允许的边。 # 换票由调用方(feature)完成后把 URL 传进来,本包只负责校验和承载。 environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_auth: ^0.1.0 core_foundation: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 + flutter_riverpod: 3.2.1 webview_flutter: ^4.14.1 dev_dependencies: diff --git a/packages/feature_auth/pubspec.yaml b/packages/feature_auth/pubspec.yaml index 3e5b252..870e14f 100644 --- a/packages/feature_auth/pubspec.yaml +++ b/packages/feature_auth/pubspec.yaml @@ -8,7 +8,7 @@ resolution: workspace # - 这里不出现任何另一个 feature_*(feature 间禁止互相依赖,见 01) # - 这里不直接出现 go_router(路由类型由 core_router re-export,见 04) environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_auth: ^0.1.0 @@ -18,8 +18,9 @@ dependencies: core_ui: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 - riverpod_annotation: ^4.0.3 + flutter_riverpod: 3.2.1 + # 与 riverpod_generator 4.0.3 配套,兼容 Harmony Flutter 固定的 meta 1.17.0。 + riverpod_annotation: 4.0.2 dev_dependencies: build_runner: ^2.4.13 @@ -27,4 +28,4 @@ dev_dependencies: flutter_test: sdk: flutter mocktail: ^1.0.5 - riverpod_generator: ^4.0.4 + riverpod_generator: 4.0.3 diff --git a/packages/feature_home/pubspec.yaml b/packages/feature_home/pubspec.yaml index 8a955f2..667e8e0 100644 --- a/packages/feature_home/pubspec.yaml +++ b/packages/feature_home/pubspec.yaml @@ -9,7 +9,7 @@ resolution: workspace # "需要" 登录信息,但它拿的是 core_auth 的会话状态,不是 feature_auth 的页面 # - 这里不直接出现 go_router(路由类型由 core_router re-export,见 04) environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: core_analytics: ^0.1.0 @@ -20,8 +20,9 @@ dependencies: core_ui: ^0.1.0 flutter: sdk: flutter - flutter_riverpod: ^3.3.2 - riverpod_annotation: ^4.0.3 + flutter_riverpod: 3.2.1 + # 与 riverpod_generator 4.0.3 配套,兼容 Harmony Flutter 固定的 meta 1.17.0。 + riverpod_annotation: 4.0.2 dev_dependencies: build_runner: ^2.4.13 @@ -29,4 +30,4 @@ dev_dependencies: flutter_test: sdk: flutter mocktail: ^1.0.5 - riverpod_generator: ^4.0.4 + riverpod_generator: 4.0.3 diff --git a/packages/native_scan/example/pubspec.yaml b/packages/native_scan/example/pubspec.yaml index 31c2f72..c8469a2 100644 --- a/packages/native_scan/example/pubspec.yaml +++ b/packages/native_scan/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none resolution: workspace environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: flutter: diff --git a/packages/native_scan/pubspec.yaml b/packages/native_scan/pubspec.yaml index 6c655c5..1432cfa 100644 --- a/packages/native_scan/pubspec.yaml +++ b/packages/native_scan/pubspec.yaml @@ -10,7 +10,7 @@ resolution: workspace # 因此本包抛的是包内自定义的 NativeScanException,由调用方转成 NativeException。 # --------------------------------------------------------------------------- environment: - sdk: ^3.12.0 + sdk: ^3.11.0 dependencies: flutter: @@ -20,7 +20,9 @@ dev_dependencies: flutter_lints: ^6.0.0 flutter_test: sdk: flutter - pigeon: ^27.3.0 + # 27.x 需要 analyzer >=10,与 Harmony 兼容的 riverpod_generator 4.0.3 + # (analyzer ^9)不能共存;26.3.1 同时兼容 Dart 3.11 与 analyzer 9。 + pigeon: 26.3.1 flutter: plugin: diff --git a/pubspec.lock b/pubspec.lock index ad8f4a4..60029c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" url: "https://pub.dev" source: hosted - version: "99.0.0" + version: "92.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7" + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "9.0.0" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: "445b77e2054fa3e8c8a8ef1b5e9e6b23bb8028fffd34b5e60eaef315b7750674" + sha256: ff4bd291778c7417fe53fe24ee0d0a1f1ffe281a2d4ea887e7094f16e36eace7 url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.0" ansi_styles: dependency: transitive description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: dart_style - sha256: a4c1ccfee44c7e75ed80484071a5c142a385345e658fd8bd7c4b5c97e7198f98 + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b url: "https://pub.dev" source: hosted - version: "3.1.8" + version: "3.1.3" dio: dependency: transitive description: @@ -316,10 +316,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: "9255e1e3ad6e38906a1b4f8287678f95f378744c5b46b1985588543f3f19046e" + sha256: e2026c72738a925a60db30258ff1f29974e40716749f3c9850aabf34ffc1a14c url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.2.1" flutter_secure_storage: dependency: transitive description: @@ -728,10 +728,10 @@ packages: dependency: transitive description: name: pigeon - sha256: f90254ef7b22db026fd8d5fd44718fd60697d50968a144a8bc251e5e95d79e10 + sha256: "7afad261c00755491a69de151009fb41fda003e69205e16ba0c354c352c2de62" url: "https://pub.dev" source: hosted - version: "27.3.0" + version: "26.3.1" platform: dependency: transitive description: @@ -816,34 +816,34 @@ packages: dependency: transitive description: name: riverpod - sha256: "17100416c51db7810c71a7bb2c34d1f881faa0074fd452afb0c4db6f8f126c76" + sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.2.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "3e275138862ccc22ed61444a1f9a840f753094c367f28f4123f50289cd204d68" + sha256: e55bc08c084a424e1bbdc303fe8ea75daafe4269b68fd0e0f6f1678413715b66 url: "https://pub.dev" source: hosted - version: "1.0.0-dev.10" + version: "1.0.0-dev.9" riverpod_annotation: dependency: transitive description: name: riverpod_annotation - sha256: "674dbb26e2db3d9253166faf4758c796af14146b8fbcf5e7102bc8a04cd359b8" + sha256: "16471a1260b94e939394d78f1c63a9350936ac4a68c9fbdab40be47268c0b04f" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.2" riverpod_generator: dependency: transitive description: name: riverpod_generator - sha256: "54d790c3fee1ae281c448801bfdbfaa9fd961a9d3998494e0fcd9ee32184d7eb" + sha256: "6f9220534d7a353b53c875ea191a84d28cb4e52ac420a66a1bd7318329d977b0" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.3" sentry: dependency: transitive description: @@ -880,10 +880,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 url: "https://pub.dev" source: hosted - version: "2.4.27" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: @@ -1173,10 +1173,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: b98656fa4461f8cc05c48a778b4d4883e60ec63e1778348f363f9bb9a477745d + sha256: ad5182eff9a550925330cb9f0cb038eddfdd5712aba8b77aa0f0400e50f6e688 url: "https://pub.dev" source: hosted - version: "4.14.0" + version: "4.12.0" webview_flutter_platform_interface: dependency: transitive description: @@ -1189,10 +1189,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: c879dd64b87c452aa84381b244d5469da57ba7e8cca6884c7b1e0d406372c12d + sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2" url: "https://pub.dev" source: hosted - version: "3.26.0" + version: "3.25.1" win32: dependency: transitive description: @@ -1234,5 +1234,5 @@ packages: source: hosted version: "2.2.4" sdks: - dart: ">=3.12.0 <4.0.0" - flutter: ">=3.44.0" + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 82c8394..88e46f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ name: conti_retail_app publish_to: none environment: - sdk: ^3.12.0 + sdk: ^3.11.0 # --------------------------------------------------------------------------- # 工作区成员。新增包时必须同步加到这里,否则 melos bootstrap 看不到它。 -- 2.54.0 From d786fc31ffdc68c7a50f53647c3a2e266f6dbece Mon Sep 17 00:00:00 2001 From: "Guangfei.Zhao" Date: Wed, 2 Sep 2026 23:32:55 +0800 Subject: [PATCH 2/5] docs: Update documentation for OHOS build and native integration details --- README.md | 12 ++++++++ docs/01-project-structure.md | 12 ++++---- docs/07-native-integration.md | 54 ++++++++++++++++++++++++++++------- docs/README.md | 4 +-- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 266aacf..739c96d 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,18 @@ melos run format # dart format --set-exit-if-changed melos run test # flutter test --coverage ``` +### OHOS(`harmony` 分支)构建 + +`enable-ohos.ps1` 只为当前 PowerShell 会话注入 Flutter_OH、ohpm、hvigor 与 DevEco 环境变量,不修改用户或系统 PATH。先在仓库根目录执行,再进入 `app` 使用已有的开发环境入口构建: + +```powershell +. .\enable-ohos.ps1 +cd app +flutter build hap --debug -t lib\main_dev.dart +``` + +构建产物为未签名 debug HAP:`app/ohos/entry/build/default/outputs/default/entry-default-unsigned.hap`。这不会创建新的 Dart `main` 文件;现有环境入口仍是 `lib/main_dev.dart`、`lib/main_uat.dart` 和 `lib/main_prod.dart`。HAP 编译通过不代表 ArkTS 原生桥接、扫码或真机安装已经完成,边界见 [docs/07-native-integration.md](docs/07-native-integration.md)。 + `analyze` 带 `--fatal-infos`。不加等于没加 lint——绝大多数 riverpod_lint 规则 报的是 info 级。 diff --git a/docs/01-project-structure.md b/docs/01-project-structure.md index b729f8e..9b1c5a7 100644 --- a/docs/01-project-structure.md +++ b/docs/01-project-structure.md @@ -30,7 +30,7 @@ conti-app/ feature_analytics/ # 经营分析:对账单、核销收入、返利、报表 feature_profile/ # 个人中心:地址、热线、客服 feature_scan/ # 扫码业务入口(VIN/车牌/二维码/条码 → 分发到对应业务) - native_scan/ # 原生插件包:扫码能力(android/ios 两端实现) + native_scan/ # 原生能力 Dart facade;Android/iOS 插件,OHOS 实现在 App entry native_media/ # 相机、相册、文件选择/上传 native_device/ # 拨号、设备信息、权限申请 ``` @@ -42,7 +42,7 @@ conti-app/ - `app` 可以依赖所有 `core_*` 和 `feature_*`。 - `feature_*` **只能**依赖 `core_*` 和 `native_*`,**不能**相互依赖(`feature_purchase` 的 `pubspec.yaml` 里不允许出现 `feature_inventory` 的 path dependency)。 - `core_*` 可以依赖 `native_*`(`core_webview` 的 JSBridge 需要调起扫码/相机/上传)。 -- `native_*` 只依赖 Flutter SDK 和 [Pigeon](https://pub.dev/packages/pigeon) 生成的代码,不依赖任何 `core_*` / `feature_*`——保证原生插件包可以脱离业务单独编译、单独测试(详见 [07-native-integration.md](./07-native-integration.md))。 +- `native_*` 只依赖 Flutter SDK 与自身的跨平台 transport,不依赖任何 `core_*` / `feature_*`。Android/iOS transport 使用 [Pigeon](https://pub.dev/packages/pigeon) 生成代码;OHOS transport 可在 facade 内使用 `MethodChannel`,其 ArkTS 落在 App 宿主 `ohos/entry`,仍不得泄漏给业务层(详见 [07-native-integration.md](./07-native-integration.md))。 `core_*` 之间原则上不互相依赖,允许的例外只有下面三条,多一条都要走评审: @@ -74,7 +74,7 @@ conti-app/ - `core_xxx`:基础设施层,不含具体业务逻辑。 - `feature_xxx`:对应一个业务域(多数是原来的某个小程序,也有全新的,如首页工作台)。 -- `native_xxx`:原生能力插件包,首版含 `android/`、`ios/` 两套原生实现目录(OHOS 不在首版范围,见 [07-native-integration.md](./07-native-integration.md))。 +- `native_xxx`:业务侧唯一可依赖的原生能力 facade。Android/iOS 原生实现位于插件包;`harmony` 分支首期 OHOS ArkTS 实现位于 App 宿主的 `ohos/entry`,见 [07-native-integration.md](./07-native-integration.md)。 ## SDK 版本基线 @@ -82,8 +82,11 @@ conti-app/ |---|---|---| | Flutter | **3.44.9** | 用 [FVM](https://fvm.app/) 锁定,仓库根目录提交 `.fvmrc` | | Dart | 随 Flutter 3.44.9 附带(3.12.x) | 具体号以 `flutter --version` 实测为准;`environment: sdk: ^3.11.0` 对整个 3.12.x 都成立 | +| Flutter_OH(仅 `harmony`) | **3.41.10-ohos-1.0.0 / Dart 3.11.5** | 仅用于 OHOS 构建;由 `enable-ohos.ps1` 临时注入当前 PowerShell PATH,不能替换 Android/iOS 的 FVM SDK | -**为什么不跟最新 stable(3.47.0 / Dart 3.13.0,2026-08-12 发布)**:鸿蒙(OpenHarmony)的 Flutter 分支适配落后于官方 stable 一段时间,虽然 OHOS 不在首版范围(见 [07-native-integration.md](./07-native-integration.md) 的「OHOS 后续演进」),但 SDK 基线要为后续接 OHOS 留出兼容窗口,所以刻意停在 3.44.9 而不是追最新。这条约束在决定升级 Flutter 版本时必须重新评估,不要因为"新版本有新特性"就单方面升。 +**OHOS SDK 边界**:OpenHarmony 的 Flutter 分支与官方 stable 不同步。Android/iOS 仍以 FVM 锁定的 Flutter 3.44.9 为基线;`harmony` 分支已用 Flutter_OH 3.41.10-ohos-1.0.0 / Dart 3.11.5 完成 HAP 编译验证。两套 SDK 不可混用,任一侧升级都必须重新验证依赖解析、生成代码和对应平台构建(见 [07-native-integration.md](./07-native-integration.md))。 + +**未来收敛条件**:Flutter_OH 上游文档计划在 **2026 年 9 月**提供 Flutter **3.44.x / Dart 3.12.x** 对齐版本;发布实际 Tag/Commit 可获取后,可评审将 `harmony` 与 Android/iOS 收敛到同一 SDK 基线。计划日期或版本号相同都不是自动放行条件;必须重新完成依赖解析、Riverpod/Pigeon 生成物核验、HAP 构建及真机原生能力验证后才能切换。 **为什么必须用 FVM 锁**:monorepo 里各人本地 Flutter 版本不一致,会导致同一份代码有人 `flutter analyze` 过、有人不过,生成代码(`build_runner` 产物)也可能不一致——这类问题排查成本远高于装一次 FVM。CI 也用 `.fvmrc` 里的版本,保证本地和流水线一致。 @@ -263,4 +266,3 @@ dart pub global activate melos - [Pigeon | Dart package](https://pub.dev/packages/pigeon) - [Drift | Dart package](https://pub.dev/packages/drift) - [Lerna(JS 生态对标工具)](https://lerna.js.org/) - diff --git a/docs/07-native-integration.md b/docs/07-native-integration.md index af304c8..e664601 100644 --- a/docs/07-native-integration.md +++ b/docs/07-native-integration.md @@ -2,13 +2,13 @@ ## 决策 -原生能力(扫码、支付、蓝牙等)统一封装成独立的 `native_*` Dart package(结构见 [01-project-structure.md](./01-project-structure.md)),跨语言接口用 **[Pigeon](https://pub.dev/packages/pigeon)**(`^27.3.0`,2026-08 快照)生成,不手写裸 `MethodChannel`/`invokeMethod` 字符串调用。 +原生能力(扫码、支付、蓝牙等)统一由独立的 `native_*` Dart package 暴露稳定公共 API(结构见 [01-project-structure.md](./01-project-structure.md))。**Android/iOS** 跨语言接口使用 [Pigeon](https://pub.dev/packages/pigeon)(锁定 `26.3.1`)生成;`harmony` 分支的首期 **OHOS** 实现是经过约束的例外:`native_*` 内部通过手写 `MethodChannel` 调用 App 宿主 `app/ohos/entry` 中的 ArkTS,而不是将 OHOS 再包装成独立 Flutter plugin。 ## 依赖 ```yaml dev_dependencies: - pigeon: ^27.3.0 + pigeon: 26.3.1 ``` ## 包结构规则 @@ -32,7 +32,28 @@ native_scan/ Classes/NativeScanPlugin.swift # 手写:插件注册入口 ``` -首版只有 `android/` 和 `ios/`(OHOS 不在首版范围,见文末「OHOS 后续演进」)。 +插件包中的原生目录目前只有 `android/` 和 `ios/`;这不是遗漏。OHOS 的首期 ArkTS 实现位于宿主 App 的 `app/ohos/entry`,见下节。 + +### 平台实现归属:Android/iOS 插件,OHOS 宿主桥接 + +``` +feature_* / core_webview(JSBridge) + ↓ 只依赖公共 Dart API + native_scan + ┌─────────┴──────────┐ + │ │ + Android / iOS OHOS(harmony) + Pigeon + plugin 手写 MethodChannel + Kotlin / Swift ↓ + app/ohos/entry ArkTS +``` + +| 平台 | Dart 到原生 transport | 原生代码归属 | 原因 | +| --- | --- | --- | --- | +| Android / iOS | Pigeon 生成的强类型桩代码 | `packages/native_scan/android`、`ios` | 复用成熟的 Flutter plugin 注册和 Kotlin/Swift 生成链路 | +| OHOS(首期) | `native_scan` 内部的手写 `MethodChannel` | `app/ohos/entry/src/main/ets/` | Pigeon 不生成 ArkTS;不手抄其 `BasicMessageChannel` / 自定义 codec 协议,降低漂移风险 | + +这不会增加 Dart `main` 文件,也不会新增 HAP module。`entry` 是已有 OHOS 应用模块;这里只是在其中增加本 App 的原生能力实现。业务代码和 H5 均不得直接调用 channel、ArkTS 或做 OHOS 平台判断。 ### `pubspec.yaml` 必须声明 plugin platforms @@ -197,24 +218,35 @@ pigeon: - Dart 生成物在根 `analysis_options.yaml` 里排除 lint(`analyzer: exclude: - "**/*.g.dart"`)。 - CI 要有一步"重新生成后 `git diff --exit-code`",防止有人改了 schema 但忘了提交生成物。 -## OHOS 后续演进 +## OHOS 当前实现策略(`harmony`) -鸿蒙(OpenHarmony)**不在首版范围**,但基线决策是为它留了口子的,这里记录清楚,避免后面接的时候重新走一遍弯路。 +OHOS 不是 Android 的另一种 plugin platform。当前 Flutter_OH 工具链没有可直接复用的 Pigeon ArkTS 生成链路;而现有 Pigeon API 在 Dart 侧使用 `BasicMessageChannel`、自定义 codec、类型 tag 和生成的错误/结果 envelope。让 ArkTS 人工复刻该私有 wire protocol,维护成本和接口漂移风险都不值得。 -接 OHOS 需要处理三件事: +因此,OHOS 在 `native_scan` 的**内部**新增一条简洁、独立的 `MethodChannel` transport。它接收和返回仅用于该 transport 的基础 Map/List/标量,再映射回既有的 `ScanOptions`、`ScanResult` 与 `NativeScanException`;Android/iOS Pigeon schema、生成物与注册逻辑不因此改变。 -1. **SDK 分支不同**:OHOS 用的是 OpenHarmony 社区维护的 Flutter 分支,版本落后于官方 stable 一段时间。这正是 [01-project-structure.md](./01-project-structure.md) 里 SDK 基线刻意停在 **3.44.9** 而不追 3.47.0 的原因——基线跑太前,OHOS 分支跟不上就接不进来。 -2. **Pigeon 没有 ArkTS 生成器**:Pigeon 官方只生成 Kotlin/Java、Swift/Objective-C、C++、GObject,**没有 ArkTS/OHOS**。所以 OHOS 侧的 channel 代码只能**手写**,需要人工保证方法名、参数结构与 Pigeon 生成的 Dart 端编解码格式一致——这是一份实打实的额外维护成本,接 OHOS 时要预留出来。 -3. **`native_*` 包要加 `ohos:` 平台声明**,并新增 `ohos/` 目录。 +ArkTS 按职责拆分,避免把业务实现写进默认入口: -在此之前,`native_*` 的公共 API 类里遇到不支持的平台,一律抛明确的 `UnsupportedPlatformException`,不静默返回空值或占位假数据——静默返回会让"这个平台其实没实现"的问题一直藏到用户手里。 +``` +app/ohos/entry/src/main/ets/ + entryability/EntryAbility.ets # 生命周期、Flutter engine 与 bridge 注册 + native_scan/NativeScanChannel.ets # 方法分发、参数/结果序列化、错误映射 + native_scan/OhosScanService.ets # 相机/扫码 API 调用 + native_scan/ScanPermissionService.ets # 相机权限检查与申请 +``` + +`NativeScanChannel` 必须集中维护 channel 名、方法名、参数、成功返回值与错误码;Dart facade 捕获 `PlatformException` 并继续转为 `NativeScanException`。取消、拒绝权限、能力不可用、超时和未知方法必须分别有明确错误码,不能返回空值或伪造扫描结果。 + +**延后抽取独立 OHOS plugin。** 只有出现第二个 OHOS App 的复用需求、扫描实现和契约已稳定、Flutter_OH plugin 注册/模板已成熟并经验证,或 SDK 化收益明确超过维护成本时,才评审将 ArkTS 从 `app/ohos/entry` 抽到 `packages/native_scan/ohos`。在此之前,不添加 `ohos:` plugin 声明或额外 `ohos/` 目录。 + +**SDK 收敛条件。** 当前 Android/iOS 使用 Flutter 3.44.9,OHOS 使用 Flutter_OH 3.41.10-ohos-1.0.0;Flutter_OH 上游文档计划在 **2026 年 9 月**提供 Flutter 3.44.x / Dart 3.12.x 对齐版本。实际 Tag/Commit 发布并验证可用后,可评审统一基线。升级前必须以该候选 SDK 完成依赖解析、Pigeon 与 Riverpod 生成物核验、`flutter build hap --debug -t lib\main_dev.dart` 和真机原生能力回归;不能仅因计划日期或版本号相同就替换工具链。 ## 使用规则 -- `pigeons/xxx_api.dart` 是**唯一手写**的接口定义文件,Dart 端和两端原生的桩代码全部由 `dart run pigeon --input pigeons/xxx_api.dart` 生成,生成产物不手动修改,改需求就改 schema 重新生成。 +- Android/iOS 的 `pigeons/xxx_api.dart` 是**唯一手写**的 Pigeon 接口定义文件;Dart 端及 Kotlin/Swift 桩代码全部由 `dart run pigeon --input pigeons/xxx_api.dart` 生成,生成产物不手动修改。OHOS 不手抄 Pigeon wire protocol,而是维护独立、文档化的 `MethodChannel` 契约。 - Dart 调原生用 `@HostApi()`;原生主动推事件给 Dart(比如扫码结果的持续回调)用 `@FlutterApi()`——不允许为了图省事用 `@HostApi()` 硬凑双向通信。 - **调用方只允许依赖 `native_*` 包 `lib/native_xxx.dart` 导出的公共 API 类**,不允许直接 import `src/generated/` 里的生成代码。调用方包括 `feature_*` 和 `core_webview`(JSBridge)。 +- `native_*` 是平台分派和异常收敛的唯一位置;Feature、`core_webview`、H5 与 Dart entrypoint 均不得直接创建 OHOS `MethodChannel` 或 import ArkTS 对应实现。 - 原生侧异常需要在生成的 host API 实现里捕获并转换成 Pigeon schema 里声明的错误类型,Dart 侧统一映射成 [05-networking.md](./05-networking.md) 里同一套 `AppException` 体系,不让原生异常类型(如 `PlatformException`)直接抛到业务代码里。 - 某一端暂未实现的能力,公共 API 类里对应平台分支抛明确的 `UnsupportedPlatformException`,不允许静默返回空值或占位假数据。 diff --git a/docs/README.md b/docs/README.md index 941322a..c1e7cbb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ Flutter APP 的分包、分层、技术选型等决策记录,按序号阅读 **这些文档是本仓库代码的约束来源。** 代码和文档不一致时,以文档为准改代码。 -首版范围为 **Android / iOS**,鸿蒙 OHOS 不在首版内(但 SDK 基线锁 3.44.9 是为后续 OHOS 适配留窗口,见 01 和 07)。 +首版正式交付范围仍为 **Android / iOS**。`harmony` 分支已用 Flutter_OH 完成 OHOS debug HAP 编译验证,但这不是生产可用结论;运行、Dart/ArkTS 桥接、相机扫码和真机验收仍须按 [07](./07-native-integration.md) 的 Gate 完成。 | 文档 | 内容 | | --- | --- | @@ -14,7 +14,7 @@ Flutter APP 的分包、分层、技术选型等决策记录,按序号阅读 | [04-routing.md](./04-routing.md) | 路由方案(go_router) | | [05-networking.md](./05-networking.md) | 网络层设计(dio) | | [06-local-storage.md](./06-local-storage.md) | 本地存储方案(Drift / secure storage / shared_preferences) | -| [07-native-integration.md](./07-native-integration.md) | 原生能力集成方式(Pigeon) | +| [07-native-integration.md](./07-native-integration.md) | 原生能力集成方式(Android/iOS Pigeon;OHOS 宿主 MethodChannel) | | [08-build-flavors.md](./08-build-flavors.md) | 多环境构建(dev/uat/prod flavor) | | [09-testing.md](./09-testing.md) | 测试策略(单元/Widget/集成测试) | | [10-webview-h5.md](./10-webview-h5.md) | Embedded H5 容器与 JSBridge(PRD §7 核心链路) | -- 2.54.0 From 782fba80bc1014060b6cc7c5e7da2d283f108b59 Mon Sep 17 00:00:00 2001 From: "Guangfei.Zhao" Date: Thu, 3 Sep 2026 11:39:04 +0800 Subject: [PATCH 3/5] refactor: remove CrashReporter and related logging components - Removed CrashReporter interface and its implementation, SentryCrashReporter. - Deleted log buffering functionality and associated LogBuffer class. - Updated LoggerAppLogger to eliminate dependencies on removed components. - Simplified logging structure to focus on AppLogger and sensitive data scrubbing. - Adjusted AppEnv to remove Sentry-related configurations. - Refactored tests to align with the new logging architecture. --- .gitlab-ci.yml | 8 +- README.md | 20 +- app/env/dev.json | 3 +- app/env/prod.json | 3 +- app/env/uat.json | 1 - app/integration_test/app_test.dart | 13 +- app/ios/FLAVORS.md | 7 +- app/lib/bootstrap.dart | 58 +-- app/lib/main.dart | 15 + app/lib/main_dev.dart | 14 - app/lib/main_prod.dart | 14 - app/lib/main_uat.dart | 14 - app/lib/src/bundled_app_env.dart | 29 ++ app/lib/src/error_observer.dart | 8 +- app/lib/src/session_observer.dart | 26 +- app/pubspec.yaml | 15 +- docs/04-routing.md | 2 +- docs/07-native-integration.md | 2 +- docs/08-build-flavors.md | 49 +- docs/11-store-context-and-session.md | 6 +- docs/13-observability-analytics.md | 448 +----------------- .../core_analytics/lib/src/analytics.dart | 2 +- packages/core_auth/lib/src/session_ports.dart | 7 +- packages/core_auth/test/core_auth_test.dart | 1 - .../core_foundation/lib/src/env/app_env.dart | 38 +- .../test/app_exception_test.dart | 38 +- packages/core_logging/lib/core_logging.dart | 14 +- .../lib/src/crash_breadcrumb_observer.dart | 38 -- .../core_logging/lib/src/crash_reporter.dart | 56 --- packages/core_logging/lib/src/log_buffer.dart | 57 --- .../lib/src/logger_app_logger.dart | 21 +- packages/core_logging/lib/src/providers.dart | 13 - packages/core_logging/lib/src/scrubber.dart | 6 +- .../lib/src/sentry_crash_reporter.dart | 71 --- .../core_logging/lib/src/sentry_scrubber.dart | 43 -- .../core_logging/lib/src/test_exception.dart | 17 - packages/core_logging/pubspec.yaml | 3 +- .../core_logging/test/core_logging_test.dart | 76 +-- pubspec.lock | 88 ---- 39 files changed, 192 insertions(+), 1152 deletions(-) create mode 100644 app/lib/main.dart delete mode 100644 app/lib/main_dev.dart delete mode 100644 app/lib/main_prod.dart delete mode 100644 app/lib/main_uat.dart create mode 100644 app/lib/src/bundled_app_env.dart delete mode 100644 packages/core_logging/lib/src/crash_breadcrumb_observer.dart delete mode 100644 packages/core_logging/lib/src/crash_reporter.dart delete mode 100644 packages/core_logging/lib/src/log_buffer.dart delete mode 100644 packages/core_logging/lib/src/sentry_crash_reporter.dart delete mode 100644 packages/core_logging/lib/src/sentry_scrubber.dart delete mode 100644 packages/core_logging/lib/src/test_exception.dart diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85cd0d2..489cc98 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -96,8 +96,6 @@ coverage-gate: - > flutter build apk --flavor $FLAVOR - -t lib/main_$FLAVOR.dart - --dart-define-from-file=env/$FLAVOR.json $EXTRA_ARGS artifacts: paths: @@ -122,12 +120,8 @@ android-prod: <<: *android-build variables: FLAVOR: prod - # release 包必须混淆 + 拆符号表,否则 Sentry 上的堆栈是一堆十六进制地址。 - # --split-debug-info 的产物要留着,sentry_dart_plugin 靠它还原堆栈。 + # release 包必须混淆 + 拆符号表,符号表保留供后续排障使用。 EXTRA_ARGS: --release --obfuscate --split-debug-info=build/symbols - after_script: - # SENTRY_AUTH_TOKEN 只从 CI 变量读,仓库里任何文件都不许出现它。 - - cd app && dart run sentry_dart_plugin artifacts: paths: - app/build/app/outputs/flutter-apk/*.apk diff --git a/README.md b/README.md index 739c96d..2d5ee49 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,12 @@ melos run gen # 3. 跑起来 cd app -flutter run --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json +flutter run --flavor dev ``` -`--flavor` / `-t` / `--dart-define-from-file` **三个参数缺一不可**: - -- 少了 `--dart-define-from-file`,`AppEnv.fromDartDefine` 会在启动瞬间抛错。 - 这是故意的——带着空 baseUrl 跑起来,问题会在第一个请求 404 时才暴露。 -- 少了 `-t`,Flutter 会去找 `lib/main.dart`,本仓库没有这个文件。 - -VS Code / Android Studio 用户建议把三条 flavor 配进 `launch.json`。 +项目只有一个 Dart 入口 `lib/main.dart`。`--flavor dev|uat|prod` 仍用于选择 +Android/iOS 的原生包变体,同时 Flutter 会通过 `appFlavor` 选择对应的内置环境 +配置。未传 flavor 时默认使用 dev,因此 DevEco 的普通 HAP 构建无需额外入口配置。 ## 环境与工具链版本 @@ -100,10 +96,10 @@ melos run test # flutter test --coverage ```powershell . .\enable-ohos.ps1 cd app -flutter build hap --debug -t lib\main_dev.dart +flutter build hap --debug ``` -构建产物为未签名 debug HAP:`app/ohos/entry/build/default/outputs/default/entry-default-unsigned.hap`。这不会创建新的 Dart `main` 文件;现有环境入口仍是 `lib/main_dev.dart`、`lib/main_uat.dart` 和 `lib/main_prod.dart`。HAP 编译通过不代表 ArkTS 原生桥接、扫码或真机安装已经完成,边界见 [docs/07-native-integration.md](docs/07-native-integration.md)。 +构建产物为未签名 debug HAP:`app/ohos/entry/build/default/outputs/default/entry-default-unsigned.hap`。Hvigor 默认使用现有的 `lib/main.dart`,并加载 dev 环境配置。HAP 编译通过不代表 ArkTS 原生桥接、扫码或真机安装已经完成,边界见 [docs/07-native-integration.md](docs/07-native-integration.md)。 `analyze` 带 `--fatal-infos`。不加等于没加 lint——绝大多数 riverpod_lint 规则 报的是 info 级。 @@ -118,7 +114,7 @@ flutter build hap --debug -t lib\main_dev.dart app/ 壳工程。唯一知道所有包的地方:环境注入、启动编排、路由聚合 packages/ core_foundation/ AppEnv / AppException 体系 / ApiCode ← 叶子包,谁都能依赖 - core_logging/ AppLogger / CrashReporter / 脱敏 + core_logging/ AppLogger / 脱敏 core_analytics/ Analytics 接口 + 事件常量表 core_storage/ Prefs(KV)。Drift 暂缓,见 SCAFFOLD-NOTES core_auth/ AppSession 四态 / TokenStorage / SessionNotifier @@ -179,5 +175,5 @@ flutter analyze → info: depend_on_referenced_packages 模板。现在调 `startScan` 会抛 `MissingPluginException`,属预期。 - **神策 SDK 未采购**:`analyticsProvider` 是 `NoopAnalytics`。接入时注意必须在 用户同意隐私政策之后再初始化。 -- **env/*.json 里全是占位域名**,`SENTRY_DSN` 全空,待运维确认。 +- **env/*.json 里全是占位域名**,待运维确认。 - **CI 镜像名未定**,`.gitlab-ci.yml` 里标了 TODO(ops)。 diff --git a/app/env/dev.json b/app/env/dev.json index 03b85cb..a91ed1f 100644 --- a/app/env/dev.json +++ b/app/env/dev.json @@ -1,7 +1,6 @@ { - "_comment": "dev 环境。构建时必须带 --dart-define-from-file=env/dev.json,否则 AppEnv.fromDartDefine 会直接抛错。所有地址都是占位符,TODO(ops) 待运维确认真实域名。", + "_comment": "dev 环境。未指定 flavor 的启动(包括 DevEco 默认 HAP 构建)使用此配置。所有地址都是占位符,TODO(ops) 待运维确认真实域名。", "API_BASE_URL": "https://api-dev.example.com", "ENABLE_LOG": true, - "SENTRY_DSN": "", "H5_ALLOWED_HOSTS": "h5-dev.example.com" } diff --git a/app/env/prod.json b/app/env/prod.json index 169e015..d32362b 100644 --- a/app/env/prod.json +++ b/app/env/prod.json @@ -1,7 +1,6 @@ { - "_comment": "prod 环境。ENABLE_LOG 必须为 false(13:生产不写控制台、不落日志文件)。TODO(ops) 待运维确认真实域名与 Sentry DSN。", + "_comment": "prod 环境。ENABLE_LOG 必须为 false,生产不写控制台日志。TODO(ops) 待运维确认真实域名。", "API_BASE_URL": "https://api.example.com", "ENABLE_LOG": false, - "SENTRY_DSN": "", "H5_ALLOWED_HOSTS": "h5.example.com" } diff --git a/app/env/uat.json b/app/env/uat.json index cc13586..7cb06d0 100644 --- a/app/env/uat.json +++ b/app/env/uat.json @@ -2,6 +2,5 @@ "_comment": "uat 环境。TODO(ops) 待运维确认真实域名与 Sentry DSN。", "API_BASE_URL": "https://api-uat.example.com", "ENABLE_LOG": true, - "SENTRY_DSN": "", "H5_ALLOWED_HOSTS": "h5-uat.example.com" } diff --git a/app/integration_test/app_test.dart b/app/integration_test/app_test.dart index bb52132..3de479d 100644 --- a/app/integration_test/app_test.dart +++ b/app/integration_test/app_test.dart @@ -7,7 +7,7 @@ // // 跑法: // flutter test integration_test/app_test.dart \ -// --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json +// --flavor dev // --------------------------------------------------------------------------- import 'package:app/bootstrap.dart'; @@ -20,7 +20,16 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('冷启动后停在登录页', (WidgetTester tester) async { - await bootstrap(AppEnv.fromDartDefine(flavor: 'dev')); + await bootstrap( + AppEnv.fromConfig( + flavor: 'dev', + values: { + 'API_BASE_URL': 'https://api-dev.example.com', + 'ENABLE_LOG': true, + 'H5_ALLOWED_HOSTS': 'h5-dev.example.com', + }, + ), + ); await tester.pumpAndSettle(); // 没有 token → SessionUnauthenticated → redirect 到 /login(04)。 diff --git a/app/ios/FLAVORS.md b/app/ios/FLAVORS.md index 57264c4..7d20d6c 100644 --- a/app/ios/FLAVORS.md +++ b/app/ios/FLAVORS.md @@ -62,7 +62,7 @@ Xcode 自己维护的二进制风格文本,手写会在下一次 Xcode 打开 6. 验证: ```bash cd app - flutter build ios --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json + flutter build ios --flavor dev ``` 三个 flavor 各跑一次,确认桌面上能同时装下三个图标、名字不同。 @@ -71,6 +71,5 @@ Xcode 自己维护的二进制风格文本,手写会在下一次 Xcode 打开 - **CocoaPods 和 flavor 无关**:`Podfile` 不需要改,pod 是按 target 装的, 不是按 configuration。但新增 configuration 后要跑一次 `pod install`,否则 会报 `Unable to find a configuration named 'Debug-dev'`。 -- **`--flavor` 和 `-t` 必须同时给**。只给 `--flavor dev` 会用默认的 - `lib/main.dart`——这个文件在本仓库里**不存在**(入口是 `main_dev.dart`), - 报错信息是找不到文件,和 flavor 看不出关系。 +- 项目固定使用 `lib/main.dart`。`--flavor` 除了选择 Scheme,还会让启动代码 + 选择同名的内置环境配置;不要再额外传 `-t` 或 `--dart-define-from-file`。 diff --git a/app/lib/bootstrap.dart b/app/lib/bootstrap.dart index 950c22f..7c26e2c 100644 --- a/app/lib/bootstrap.dart +++ b/app/lib/bootstrap.dart @@ -1,6 +1,6 @@ -/// 三个入口共用的启动编排。 +/// 唯一入口共用的启动编排。 /// -/// 来源:08(多环境)、13(Sentry / 日志)、12(全局错误)、03(Riverpod 装配)。 +/// 来源:08(多环境)、12(全局错误)、03(Riverpod 装配)。 library; import 'package:app/src/app_widget.dart'; @@ -20,12 +20,11 @@ import 'package:feature_auth/feature_auth.dart'; import 'package:feature_home/feature_home.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; /// TODO(app): 换成 package_info_plus 读真实版本,现在写死会在灰度期骗人。 const String _appVersion = '1.0.0+1'; -/// 全部三个 main_*.dart 都只调这一个函数。 +/// 唯一 `main.dart` 调用的启动函数。 /// /// --------------------------------------------------------------------------- /// 这个函数是**整个仓库唯一一处知道所有包的地方**。各个 core_* 只声明自己需要 @@ -40,14 +39,7 @@ Future bootstrap(AppEnv env) async { // 装一次全局单例,供拿不到 Ref 的 TokenRefresher 用;重复调用会抛错。 AppEnv.install(env); - final LogBuffer buffer = LogBuffer(); - final AppLogger logger = LoggerAppLogger(env: env, buffer: buffer); - - // dsn 为空(dev 默认)时连 SDK 都不初始化:开发期的噪音不该混进线上数据。 - final bool sentryEnabled = env.sentryDsn.isNotEmpty; - final CrashReporter reporter = sentryEnabled - ? const SentryCrashReporter() - : const NoopCrashReporter(); + final AppLogger logger = LoggerAppLogger(env: env); final Prefs prefs = Prefs(); final ClientInfo clientInfo = ClientInfo( @@ -62,13 +54,11 @@ Future bootstrap(AppEnv env) async { Widget buildApp() { return ProviderScope( - observers: [ErrorObserver(logger: logger, reporter: reporter)], + observers: [ErrorObserver(logger: logger)], overrides: [ // --- 基础设施:值已经在上面造好了,直接注入 ----------------------- appEnvProvider.overrideWithValue(env), // core_foundation appLoggerProvider.overrideWithValue(logger), // core_logging - logBufferProvider.overrideWithValue(buffer), // core_logging(和 beforeSend 同一实例) - crashReporterProvider.overrideWithValue(reporter), // core_logging prefsProvider.overrideWithValue(prefs), // core_storage clientInfoProvider.overrideWithValue(clientInfo), // core_network 端口 // --- 端口:接口在 core_*,实现在能依赖 core_network 的这一层 -------- @@ -87,10 +77,7 @@ Future bootstrap(AppEnv env) async { ), sessionObserversProvider.overrideWith( (Ref ref) => [ - AppSessionObserver( - analytics: ref.watch(analyticsProvider), - reporter: ref.watch(crashReporterProvider), - ), + AppSessionObserver(analytics: ref.watch(analyticsProvider)), ], ), @@ -98,16 +85,8 @@ Future bootstrap(AppEnv env) async { appRoutesProvider.overrideWith( (Ref ref) => [...buildAuthRoutes(), ...buildHomeRoutes()], ), - navigatorObserversProvider.overrideWith( - (Ref ref) => [ - CrashBreadcrumbObserver(ref.watch(crashReporterProvider)), - ], - ), routeReporterProvider.overrideWith( - (Ref ref) => AppRouteReporter( - logger: ref.watch(appLoggerProvider), - reporter: ref.watch(crashReporterProvider), - ), + (Ref ref) => AppRouteReporter(logger: ref.watch(appLoggerProvider)), ), // --- 日志出口:core_network 只知道"往这里写字符串" ------------------- @@ -123,26 +102,5 @@ Future bootstrap(AppEnv env) async { ); } - if (!sentryEnabled) { - runApp(buildApp()); - return; - } - - await SentryFlutter.init( - (SentryFlutterOptions options) { - options.dsn = env.sentryDsn; - options.environment = env.flavor.name; - options.release = 'conti-retail-app@$_appVersion'; - options.tracesSampleRate = env.isProd ? 0.1 : 1.0; - // 合规红线:不自动带用户 IP / 请求头 / cookie。 - options.sendDefaultPii = false; - options.beforeBreadcrumb = scrubBreadcrumb; - options.beforeSend = buildScrubEvent(buffer); - options.debug = false; - }, - // 用 appRunner 而不是自己写 FlutterError.onError:SentryFlutter 已经在 - // 里面装好了 Flutter / PlatformDispatcher / Zone 三层钩子,再手写一遍 - // 会**每个异常上报两次**(13)。 - appRunner: () => runApp(buildApp()), - ); + runApp(buildApp()); } diff --git a/app/lib/main.dart b/app/lib/main.dart new file mode 100644 index 0000000..7920b65 --- /dev/null +++ b/app/lib/main.dart @@ -0,0 +1,15 @@ +/// 应用唯一 Dart 入口。 +/// +/// Android、iOS 与 Web 用 `--flavor dev|uat|prod` 选择环境;未提供 flavor +/// 的平台(例如 DevEco 的默认 HAP 构建)统一使用 dev。 +library; + +import 'package:app/bootstrap.dart'; +import 'package:app/src/bundled_app_env.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show appFlavor; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await bootstrap(await loadBundledAppEnv(flavor: appFlavor)); +} diff --git a/app/lib/main_dev.dart b/app/lib/main_dev.dart deleted file mode 100644 index 9040d16..0000000 --- a/app/lib/main_dev.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// dev 环境入口。 -/// -/// 跑法(08): -/// ``` -/// flutter run --flavor dev -t lib/main_dev.dart --dart-define-from-file=env/dev.json -/// ``` -/// flavor 名写死在这里而不是从 dart-define 读——"用 dev 的入口配了别的环境的 -/// json"这种事故必须在代码里看得见。 -library; - -import 'package:app/bootstrap.dart'; -import 'package:core_foundation/core_foundation.dart'; - -Future main() => bootstrap(AppEnv.fromDartDefine(flavor: 'dev')); diff --git a/app/lib/main_prod.dart b/app/lib/main_prod.dart deleted file mode 100644 index 6f3b7b4..0000000 --- a/app/lib/main_prod.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// prod 环境入口。 -/// -/// 跑法(08): -/// ``` -/// flutter run --flavor prod -t lib/main_prod.dart --dart-define-from-file=env/prod.json -/// ``` -/// flavor 名写死在这里而不是从 dart-define 读——"用 prod 的入口配了别的环境的 -/// json"这种事故必须在代码里看得见。 -library; - -import 'package:app/bootstrap.dart'; -import 'package:core_foundation/core_foundation.dart'; - -Future main() => bootstrap(AppEnv.fromDartDefine(flavor: 'prod')); diff --git a/app/lib/main_uat.dart b/app/lib/main_uat.dart deleted file mode 100644 index c08e309..0000000 --- a/app/lib/main_uat.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// uat 环境入口。 -/// -/// 跑法(08): -/// ``` -/// flutter run --flavor uat -t lib/main_uat.dart --dart-define-from-file=env/uat.json -/// ``` -/// flavor 名写死在这里而不是从 dart-define 读——"用 uat 的入口配了别的环境的 -/// json"这种事故必须在代码里看得见。 -library; - -import 'package:app/bootstrap.dart'; -import 'package:core_foundation/core_foundation.dart'; - -Future main() => bootstrap(AppEnv.fromDartDefine(flavor: 'uat')); diff --git a/app/lib/src/bundled_app_env.dart b/app/lib/src/bundled_app_env.dart new file mode 100644 index 0000000..83f2041 --- /dev/null +++ b/app/lib/src/bundled_app_env.dart @@ -0,0 +1,29 @@ +/// 随 App 打包的环境配置加载器。 +library; + +import 'dart:convert'; + +import 'package:core_foundation/core_foundation.dart'; +import 'package:flutter/foundation.dart' show FlutterError; +import 'package:flutter/services.dart'; + +/// 根据 Flutter 的 flavor 加载对应环境配置;未指定时默认开发环境。 +Future loadBundledAppEnv({String? flavor}) async { + final String selectedFlavor = flavor ?? AppFlavor.dev.name; + final String assetPath = 'env/$selectedFlavor.json'; + + try { + final String jsonObj = await rootBundle.loadString(assetPath); + final decoded = jsonDecode(jsonObj); + if (decoded is! Map) { + throw const FormatException('根节点必须是 JSON 对象'); + } + return AppEnv.fromConfig(flavor: selectedFlavor, values: decoded.cast()); + } on StateError { + rethrow; + } on FormatException catch (error) { + throw StateError('环境配置 $assetPath 格式错误:${error.message}'); + } on FlutterError catch (error) { + throw StateError('无法加载环境配置 $assetPath:${error.message}'); + } +} diff --git a/app/lib/src/error_observer.dart b/app/lib/src/error_observer.dart index de026dd..8b9ecab 100644 --- a/app/lib/src/error_observer.dart +++ b/app/lib/src/error_observer.dart @@ -10,7 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// /// --------------------------------------------------------------------------- /// 它是**兜底**,不是主路径:UI 该显示的错误由 `AsyncValueView` 负责,这里只 -/// 负责"这个异常有没有人处理过"之外的另一件事——落日志和上报。 +/// 负责"这个异常有没有人处理过"之外的另一件事——落日志。 /// /// 三类要**主动排除**,否则线上告警会被噪音淹没: /// - [BusinessException]:后端明确告诉我们"这个操作不允许",是预期内的流程 @@ -20,14 +20,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; /// --------------------------------------------------------------------------- final class ErrorObserver extends ProviderObserver { /// 构造。 - ErrorObserver({required this.logger, required this.reporter}); + ErrorObserver({required this.logger}); /// 日志出口。 final AppLogger logger; - /// 崩溃上报出口。 - final CrashReporter reporter; - @override void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) { final String name = context.provider.name ?? context.provider.runtimeType.toString(); @@ -42,6 +39,5 @@ final class ErrorObserver extends ProviderObserver { } logger.e('provider 失败 $name', error: error, stackTrace: stackTrace); - reporter.report(error, stackTrace, extra: {'provider': name}); } } diff --git a/app/lib/src/session_observer.dart b/app/lib/src/session_observer.dart index a65d2c8..a0614c6 100644 --- a/app/lib/src/session_observer.dart +++ b/app/lib/src/session_observer.dart @@ -1,4 +1,4 @@ -/// 会话事件的旁路接线:埋点身份、崩溃上报的用户上下文、路由错误上报。 +/// 会话事件的旁路接线:埋点身份与路由错误日志。 /// /// 这些都是 core_auth 声明的端口(`session_ports.dart` / `core_router/ports.dart`) /// 的实现——core_auth 不能依赖 core_analytics / core_logging,所以实现落在这里。 @@ -9,26 +9,20 @@ import 'package:core_auth/core_auth.dart'; import 'package:core_logging/core_logging.dart'; import 'package:core_router/core_router.dart'; -/// 把会话变化广播给埋点和崩溃上报。 +/// 把会话变化广播给埋点。 class AppSessionObserver implements SessionObserver { /// 构造。 - const AppSessionObserver({required this.analytics, required this.reporter}); + const AppSessionObserver({required this.analytics}); /// 埋点。 final Analytics analytics; - /// 崩溃上报。 - final CrashReporter reporter; - @override void onUserIdentified(UserContext user) { analytics.identify(user.userId); analytics.registerSuperProperties({ AnalyticsSuperProperty.roleCode: user.roleCode, }); - // 只传 userId,不传手机号——Sentry 侧 sendDefaultPii = false 的前提就是 - // 我们自己也不往里塞 PII。 - reporter.setUser(user.userId); } @override @@ -37,7 +31,6 @@ class AppSessionObserver implements SessionObserver { analytics.registerSuperProperties({ AnalyticsSuperProperty.storeId: store.storeId, }); - reporter.setTag('storeId', '${store.storeId}'); } @override @@ -46,7 +39,6 @@ class AppSessionObserver implements SessionObserver { analytics.track(AnalyticsEvent.logout, {AnalyticsParam.reason: reason.name}); // 门店设备是共用的:不 reset,下一个人的数据会串到上一个人身上。 analytics.reset(); - reporter.clearUser(); } @override @@ -57,26 +49,18 @@ class AppSessionObserver implements SessionObserver { } } -/// 路由未命中时上报。 +/// 路由未命中时记录日志。 class AppRouteReporter implements RouteReporter { /// 构造。 - const AppRouteReporter({required this.logger, required this.reporter}); + const AppRouteReporter({required this.logger}); /// 日志。 final AppLogger logger; - /// 崩溃上报。 - final CrashReporter reporter; - @override void onRouteNotFound(String location) { // 记路径不记 query——H5 相关路径的 query 里带票据(13 §脱敏)。 final String path = Uri.tryParse(location)?.path ?? location; logger.w('路由未命中: $path'); - reporter.report( - StateError('route not found'), - StackTrace.current, - extra: {AnalyticsParam.path: path}, - ); } } diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 9b28735..861af41 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -26,7 +26,6 @@ dependencies: flutter_riverpod: 3.2.1 intl: any native_scan: ^0.1.0 - sentry_flutter: ^9.26.0 dev_dependencies: flutter_lints: ^6.0.0 @@ -34,19 +33,13 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - sentry_dart_plugin: ^3.4.0 # 只为测试拿 InMemorySharedPreferencesAsync,不在 lib/ 里出现。 shared_preferences_platform_interface: ^2.4.2 flutter: uses-material-design: true generate: true - -# release 构建后由 CI 调用 `dart run sentry_dart_plugin` 上传符号表。 -# 见 08 §release 构建 和 13 §崩溃上报。 -sentry: - upload_debug_symbols: true - upload_source_maps: false - project: conti-retail-app - org: continental - # auth_token 只从 CI 变量 SENTRY_AUTH_TOKEN 读,绝不写进仓库 + assets: + - env/dev.json + - env/uat.json + - env/prod.json diff --git a/docs/04-routing.md b/docs/04-routing.md index 6d1a151..244570d 100644 --- a/docs/04-routing.md +++ b/docs/04-routing.md @@ -37,7 +37,7 @@ final goRouterProvider = Provider((ref) { final router = GoRouter( navigatorKey: rootNavigatorKey, // 全局 dialog / 顶层跳转需要它 initialLocation: '/home', - observers: [NavigationObserver(ref.read(crashReporterProvider))], // 崩溃前的页面路径,见 13 + observers: const [], // 路由观测由 app 层按需注入 redirect: (context, state) { // read 不是 watch:这里只要当前值,订阅由下面的 listen 负责 final auth = ref.read(authStateProvider); diff --git a/docs/07-native-integration.md b/docs/07-native-integration.md index e664601..81b6fd8 100644 --- a/docs/07-native-integration.md +++ b/docs/07-native-integration.md @@ -238,7 +238,7 @@ app/ohos/entry/src/main/ets/ **延后抽取独立 OHOS plugin。** 只有出现第二个 OHOS App 的复用需求、扫描实现和契约已稳定、Flutter_OH plugin 注册/模板已成熟并经验证,或 SDK 化收益明确超过维护成本时,才评审将 ArkTS 从 `app/ohos/entry` 抽到 `packages/native_scan/ohos`。在此之前,不添加 `ohos:` plugin 声明或额外 `ohos/` 目录。 -**SDK 收敛条件。** 当前 Android/iOS 使用 Flutter 3.44.9,OHOS 使用 Flutter_OH 3.41.10-ohos-1.0.0;Flutter_OH 上游文档计划在 **2026 年 9 月**提供 Flutter 3.44.x / Dart 3.12.x 对齐版本。实际 Tag/Commit 发布并验证可用后,可评审统一基线。升级前必须以该候选 SDK 完成依赖解析、Pigeon 与 Riverpod 生成物核验、`flutter build hap --debug -t lib\main_dev.dart` 和真机原生能力回归;不能仅因计划日期或版本号相同就替换工具链。 +**SDK 收敛条件。** 当前 Android/iOS 使用 Flutter 3.44.9,OHOS 使用 Flutter_OH 3.41.10-ohos-1.0.0;Flutter_OH 上游文档计划在 **2026 年 9 月**提供 Flutter 3.44.x / Dart 3.12.x 对齐版本。实际 Tag/Commit 发布并验证可用后,可评审统一基线。升级前必须以该候选 SDK 完成依赖解析、Pigeon 与 Riverpod 生成物核验、`flutter build hap --debug` 和真机原生能力回归;不能仅因计划日期或版本号相同就替换工具链。 ## 使用规则 diff --git a/docs/08-build-flavors.md b/docs/08-build-flavors.md index 2714bc4..f943f62 100644 --- a/docs/08-build-flavors.md +++ b/docs/08-build-flavors.md @@ -17,11 +17,11 @@ App 侧维护 **3 个 flavor:`dev` / `uat` / `prod`**,环境划分与现有 ## 使用规则 -- 每个 flavor 对应一个独立的 Dart 入口文件(`main_dev.dart`/`main_uat.dart`/`main_prod.dart`),三者都只是设置好环境标识后调用同一个共享的 `bootstrap()` 启动函数,不允许在入口文件里写业务逻辑分支。 -- 环境相关的可变配置(API base URL、是否开启日志等,见 [05-networking.md](./05-networking.md) 的 `appEnvProvider`)通过 `--dart-define-from-file=env/{flavor}.json` 注入,不写死在代码里、也不用 `if (flavor == 'dev')` 这种运行时字符串判断来分支配置。 +- 全项目只保留 `lib/main.dart` 一个 Dart 入口。Flutter 的 `appFlavor` 自动对应 `--flavor`,启动代码据此加载打包的 `env/{flavor}.json`;未传 flavor 时默认 dev,DevEco 不需要额外入口配置。 +- 环境配置只包含可公开随包发布的 API 地址、日志开关与 H5 host;不在 Dart 代码中写死,也不要求开发者为入口文件额外传 `-t` 或 `--dart-define-from-file`。 - `env/*.json` 只包含非敏感配置(API 地址等);密钥类配置(如第三方 SDK App Key)通过 CI 变量在构建时注入,不提交进仓库。 - Android 侧用 Gradle `productFlavors` 区分 `applicationIdSuffix`/图标/`versionNameSuffix`;iOS 侧用对应的 xcconfig + Scheme 区分 `Bundle Identifier`/图标;两端 flavor 名称必须完全一致(`dev`/`uat`/`prod`),不允许两端用不同命名。 -- CI 流水线阶段固定为:`melos run analyze` → `melos run test` → 按 flavor `flutter build apk/ipa --flavor {flavor} --dart-define-from-file=env/{flavor}.json` → 上传对应分发渠道。`prod` flavor 的构建触发条件是打 tag,不是每次 push 都触发(避免误发生产包)。 +- CI 流水线阶段固定为:`melos run analyze` → `melos run test` → 按 flavor `flutter build apk/ipa --flavor {flavor}` → 上传对应分发渠道。`prod` flavor 的构建触发条件是打 tag,不是每次 push 都触发(避免误发生产包)。 ## 用 `applicationIdSuffix` 而不是覆盖 `applicationId` @@ -41,15 +41,13 @@ productFlavors { ```bash fvm flutter build appbundle \ - --flavor prod --target lib/main_prod.dart \ - --dart-define-from-file=env/prod.json \ + --flavor prod \ --obfuscate --split-debug-info=build/symbols/$CI_COMMIT_TAG ``` - `--obfuscate` 混淆 Dart 符号名,`--split-debug-info` 把调试符号剥离到单独目录(同时显著减小包体)。 - **两个参数必须一起用**,只写 `--obfuscate` 不写 `--split-debug-info` 会被工具链拒绝。 -- **符号表必须归档,并且构建完立刻上传到 Sentry**:混淆后崩溃堆栈是不可读的乱码。CI 在 build 之后紧跟一条 `fvm dart run sentry_dart_plugin`,把 Dart 符号表、Android mapping、iOS dSYM 一起传上去(见 [13-observability-analytics.md](./13-observability-analytics.md))。**上传时的 `release` 必须和 App 里 `options.release` 严格一致**,对不上的表现是"传了但堆栈还是混淆的",且后台不报错。 -- 同时把 `build/symbols/` 按 `版本号+构建号` 归档为 CI artifact 保留至少 1 年,作为 Sentry 侧数据过期或服务不可用时的兜底。**丢了符号表 = 那个版本的所有线上崩溃永远无法定位**,这是个不可逆的失误。 +- 符号表必须按 `版本号+构建号` 归档为 CI artifact 保留至少 1 年,供后续排障或未来选择的崩溃平台使用。 - 符号表目录按版本号区分(用 tag 或 `versionName+versionCode`),不能所有版本堆一个目录。 ## 版本号规则 @@ -113,8 +111,7 @@ build_ios_uat: stage: build tags: [macos] # 只有 mac runner 有这个 tag script: - - fvm flutter build ipa --flavor uat --target lib/main_uat.dart \ - --dart-define-from-file=env/uat.json --export-options-plist=ios/ExportOptions-uat.plist + - fvm flutter build ipa --flavor uat --export-options-plist=ios/ExportOptions-uat.plist ``` ## 内测分发渠道:Firebase 在国内可达性存疑 @@ -154,13 +151,11 @@ Android 的 flavor 源集目录(`src/{flavor}/`)会自动生效;iOS 没有 - **iOS 构建 runner 方案(A/B/C 选哪个)**——阻塞 iOS 发版,优先级最高。 - 内测分发渠道的最终选型与托管位置。 -- 是否引入 Firebase(影响崩溃上报选型,见 [13-observability-analytics.md](./13-observability-analytics.md))。 - Android 上架渠道清单(华为/小米/OPPO/vivo 各家应用市场是否都要上,各家的加固/隐私合规要求不同)。 ## 参考链接 - [Flutter 官方 Flavors 文档](https://docs.flutter.dev/deployment/flavors) -- [--dart-define-from-file 官方说明](https://docs.flutter.dev/deployment/flavors#configuration-approaches) - [Flutter: 混淆 Dart 代码](https://docs.flutter.dev/deployment/obfuscate) - [Android: 从命令行构建并签名](https://developer.android.com/build/building-cmdline) - [fastlane match(证书管理)](https://docs.fastlane.tools/actions/match/) @@ -179,8 +174,7 @@ Android 的 flavor 源集目录(`src/{flavor}/`)会自动生效;iOS 没有 1. **Android `productFlavors`**:在 `android/app/build.gradle` 里声明多套 `applicationId`/`versionNameSuffix`/资源目录,编译时用 `--flavor` 选择其中一套。 2. **iOS Scheme + xcconfig**:iOS 没有 Gradle 那样的单文件配置,而是通过 Xcode 里多个 Build Configuration(对应不同的 `.xcconfig` 文件设置 Bundle ID 等)+ 多个 Scheme 组合实现同样的效果,`flutter build ipa --flavor xxx` 背后就是选中同名 Scheme。 -3. **Dart 入口文件(`main_xxx.dart`)**:flavor 决定的是"编译出什么样的原生外壳",Dart 代码本身默认只有一个 `main.dart` 入口——项目约定用多个入口文件对应各 flavor,让每个 flavor 能设置不同的启动参数(比如传给 `bootstrap()` 一个环境枚举)。 -4. **`--dart-define-from-file`**:flavor 解决的是原生层面的差异(图标、包名),但 API 地址这类 Dart 侧读取的配置,用编译期注入的 JSON 文件解决,避免打包进一个写死 `http://dev-api...` 的字符串常量。 +3. **Dart 入口与环境配置**:Dart 固定从 `main.dart` 启动,Flutter 自动把 `--flavor` 暴露为 `appFlavor`;启动代码按其加载内置 JSON 配置。未提供 flavor 时用 dev,适配 DevEco 的默认构建。 ### 使用示例 @@ -225,30 +219,21 @@ android { } ``` -共享启动入口 + 各 flavor 的 Dart 入口文件: +唯一 Dart 入口: ```dart -// lib/bootstrap.dart —— 三个 flavor 共用的启动逻辑 -Future bootstrap(AppEnv env) async { - runApp(ProviderScope( - overrides: [appEnvProvider.overrideWithValue(env)], - child: const App(), - )); +// lib/main.dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await bootstrap(await loadBundledAppEnv(flavor: appFlavor)); } ``` -```dart -// lib/main_dev.dart -void main() => bootstrap(AppEnv.fromDartDefine(name: 'dev')); -``` - 构建命令: ```bash fvm flutter build apk \ - --flavor dev \ - --target lib/main_dev.dart \ - --dart-define-from-file=env/dev.json + --flavor dev ``` GitLab CI 片段(衔接现有 Runner;注意 Android 和 iOS 必须落到不同的 runner 上): @@ -266,7 +251,7 @@ build_android_dev: script: - melos run analyze - melos run test - - flutter build apk --flavor dev --target lib/main_dev.dart --dart-define-from-file=env/dev.json + - flutter build apk --flavor dev artifacts: paths: [build/app/outputs/flutter-apk/app-dev-release.apk] rules: @@ -276,8 +261,7 @@ build_android_prod: <<: *flutter_base stage: build script: - - flutter build appbundle --flavor prod --target lib/main_prod.dart - --dart-define-from-file=env/prod.json + - flutter build appbundle --flavor prod --build-name=${CI_COMMIT_TAG#v} --build-number=$CI_PIPELINE_ID --obfuscate --split-debug-info=build/symbols/$CI_COMMIT_TAG artifacts: @@ -292,8 +276,7 @@ build_ios_prod: stage: build tags: [macos] # 必须是 mac runner,Linux runner 跑不了,见上文「iOS 构建链路」 script: - - fvm flutter build ipa --flavor prod --target lib/main_prod.dart - --dart-define-from-file=env/prod.json + - fvm flutter build ipa --flavor prod --build-name=${CI_COMMIT_TAG#v} --build-number=$CI_PIPELINE_ID --obfuscate --split-debug-info=build/symbols/$CI_COMMIT_TAG rules: diff --git a/docs/11-store-context-and-session.md b/docs/11-store-context-and-session.md index 441b760..14f6b12 100644 --- a/docs/11-store-context-and-session.md +++ b/docs/11-store-context-and-session.md @@ -145,8 +145,7 @@ Future switchStore(int targetStoreId) async { // ── 7. 记住这次选择,供下次冷启动预选 ──────────────── await ref.read(prefsProvider).setInt('last_store_id', newStore.storeId); - // ── 8. 同步观测上下文(见 13)──────────────────────── - ref.read(crashReporterProvider).setTag('storeId', '${newStore.storeId}'); + // ── 8. 同步埋点上下文 ────────────────────────────────── ref.read(analyticsProvider).registerSuperProperties({'storeId': newStore.storeId}); // 切店事件本身由后端从 /stores/{id}/switch 的接口日志出,客户端不重复上报,见 13 } @@ -158,7 +157,7 @@ Future switchStore(int targetStoreId) async { | 3 在 5 之前 | H5 页面里可能有在途请求。先 `about:blank` 停掉,再换上下文,否则旧门店的 H5 请求会带着新门店的票据回来 | | 4 在 5 之前 | 缓存表带 `storeId`(见 06),但**清理和新上下文之间不能有窗口期**:如果先落新上下文,provider 立刻失效并重新请求,可能在清理完成前就把新数据写进去,然后被 `clearBusinessCache()` 一起删掉 | | 6 在 5 之后 | 清栈时目标页面(首页)要用新上下文渲染 | -| 8 在 5 之后 | 观测上下文要和业务上下文保持一致;漏了这一步的表现是**切店后的崩溃和埋点还挂在旧门店名下**,按门店维度分析时数据是错的,而且错得很隐蔽 | +| 8 在 5 之后 | 埋点上下文要和业务上下文保持一致;漏了这一步会让切店后的埋点还挂在旧门店名下,按门店维度分析时数据会悄悄出错 | **关于步骤 0(未完成写操作)**:切店时用户可能正在提交订单或上传图片。默认策略是**阻止切换并提示**,而不是静默取消——取消一个已经发出去的下单请求,客户端不知道服务端到底成没成。`pendingWriteProvider` 由发起写操作的 feature 自己注册/注销。 @@ -190,7 +189,6 @@ Future logout({LogoutReason reason = LogoutReason.userInitiated}) async { ref.read(analyticsProvider) ..track(AnalyticsEvent.logout, {'reason': reason.name}) // 报完再 reset,顺序不能反 ..reset(); - ref.read(crashReporterProvider).setUser(''); // 6. 兜底:清掉所有 provider 缓存 ref.invalidate(...); // 或在 ProviderScope 层重建,见下文 diff --git a/docs/13-observability-analytics.md b/docs/13-observability-analytics.md index 497b1b7..098906a 100644 --- a/docs/13-observability-analytics.md +++ b/docs/13-observability-analytics.md @@ -1,442 +1,22 @@ -# 13. 可观测性与埋点 - -## 为什么单独一篇 - -PRD §21.4 和 §22.1 有明确要求(主链路 Trace ID、H5 打开/关闭/失败事件、关键业务审计日志、9 类埋点事件),但 01-12 里完全没有落点。同时,**App 侧的可观测性是排查线上问题唯一的手段**——后端有 ELK 可以查日志,App 装在几百家门店的员工手机上,没有上报就等于全盲。 - -这一篇定三件事:**崩溃上报**、**日志规范**、**埋点规范**。 +# 13. 日志与埋点 ## 决策 -| 项 | 决策 | -|---|---| -| 崩溃上报 | **[sentry_flutter](https://pub.dev/packages/sentry_flutter) `^9.26.0`**(官方 verified publisher),配套 [sentry_dart_plugin](https://pub.dev/packages/sentry_dart_plugin) `^3.4.0` 上传符号表 | -| Sentry 部署形态 | **自建优先**(`sentry.io` SaaS 是跨境上报),**待确认** | -| 本地日志 | **[logger](https://pub.dev/packages/logger) `^2.7.0`**,封装在 `core_logging` 的 `AppLogger` 后面 | -| 客户端埋点 | **[神策 `sensors_analytics_flutter_plugin`](https://pub.dev/packages/sensors_analytics_flutter_plugin) `^4.2.3`**(官方 verified publisher `sensorsdata.cn`;团队过往项目用过,本项目待正式确认) | -| 业务埋点 | **以后端为主**,客户端只补后端看不到的那部分 | -| 链路关联 | 客户端生成 `X-Trace-Id`(见 05),写入本地日志并作为崩溃上报的自定义字段 | +首期不接入任何崩溃上报 SDK,也不向第三方传输异常、用户或门店上下文。业务代码通过 +`core_logging` 输出经过脱敏的本地日志;行为分析继续通过 `core_analytics` 的抽象接口接入。 -## 一、崩溃上报:Sentry +## 日志规则 -### 为什么不是 Bugly +- `AppLogger` 是唯一日志入口;请求日志和业务日志都必须经 `scrubber.dart` 脱敏。 +- dev 输出 debug 级别日志,uat 输出 info 及以上,prod 不输出控制台日志。 +- 日志不持久化、不上传;不保留无消费者的崩溃黑匣子缓存。 +- Provider 失败和未知路由仅记录脱敏日志。业务拒绝、登录失效和主动取消属于预期流程, + 不作为错误日志噪音处理。 -Bugly 是团队过往项目用过的方案,国内可达性没问题,本来是很自然的默认选项。**否掉它的理由只有一条,但这一条是决定性的:Bugly 没有上传 Dart 符号表的能力。** +## 埋点 -- Flutter App 的**绝大多数异常是 Dart 异常**(`setState` 期间抛错、null check、JSON 解析失败),不是原生崩溃。Bugly 只能把它们当"自定义异常"收下,存成一段字符串堆栈。 -- [08-build-flavors.md](./08-build-flavors.md) 要求 release 必须 `--obfuscate --split-debug-info`。两者相加的结果是:**线上占比最大的那一半崩溃,在 Bugly 后台是一串读不出来的混淆符号**,只能人工把堆栈拷出来跑 `flutter symbolize` 还原。 +`Analytics` 仍由 app 层注入。会话建立时设置用户与角色维度,切店时更新门店维度,登出时 +先记录登出事件再 reset,避免共用门店设备上的用户上下文串联。 -Bugly 在原生侧(Java/Kotlin 异常、SIGSEGV、ANR、iOS crash)确实做得好,能自动符号化。但它强的正好是我们占比小的那一半。 - -其余差别一并记录在此,作为决策存档: - -| | 腾讯 Bugly | Sentry | -|---|---|---| -| **Dart 异常堆栈还原** | **做不到** | **做得到**(`sentry_dart_plugin` 自动上传) | -| Flutter 官方 SDK | **没有**,只有 Android/iOS 原生 SDK,pub.dev 上只有 `flutter_bugly` 1.1.1、`bugly_pro_flutter` 0.4.21 两个 unverified 社区插件 | **有**,官方维护、13 天前刚发版 | -| 原生崩溃 | 强项,自动符号化 | 支持,mapping/dSYM 由同一个插件上传 | -| 接入成本 | 要自己写 `native_crash`(Pigeon + 几十行 Kotlin/Swift) | `pubspec.yaml` 加两行 | -| 国内可达性 | 无问题 | **自建无问题;SaaS 是跨境上报**,见下 | -| 运维成本 | 无 | 自建的话有(存储、升级、告警) | -| 账号/合同 | 本项目**没有**现成的,要新申请 | 同样要新建 | - -代价是运维:Sentry 这条路把"接入成本"换成了"部署成本"。这是这次选型唯一真正付出的东西。 - -注意最后一行:**本项目在两边都没有既有账号或合同**,所以"沿用现成的"这个通常最有分量的理由,在这次选型里不成立——两条路的启动成本都要从零算。 - -**连带影响:不再需要 `native_crash` 这个包。** [07-native-integration.md](./07-native-integration.md) 里的 Pigeon 包只剩 `native_scan` / `native_media`。 - -### 唯一还没定的:自建还是 SaaS - -这一条**必须在开工前定**,它决定的不只是可达性,还有合规: - -- **自建(推荐)**:崩溃数据不出境,门店网络下上报可靠,长期成本可控。代价是要一套内网 K8s/VM 资源和运维承接方。 -- **`sentry.io` SaaS**:零运维,但崩溃报告里带着 `userId`、`storeId`、面包屑和日志片段,属于**数据出境**,要走合规评估;同时门店网络访问境外服务的丢报率无法预估。 - -需要在讨论时明确的:有没有可用的内网资源、谁运维、以及法务对崩溃数据出境的口径。**在结论出来之前,`SENTRY_DSN` 走 `--dart-define-from-file`(见 08),代码里不写死任何地址——换 DSN 不需要改一行代码。** - -### 依赖与初始化 - -```yaml -dependencies: - sentry_flutter: ^9.26.0 - -dev_dependencies: - sentry_dart_plugin: ^3.4.0 -``` - -```dart -// main.dart —— 崩溃上报必须在最早期初始化,晚一步就漏掉启动期崩溃 -await SentryFlutter.init( - (options) { - options.dsn = env.sentryDsn; // 来自 --dart-define-from-file,见 08 - options.environment = env.flavorName; // dev / uat / prod 分开看,否则测试数据污染线上崩溃率 - options.release = '${env.appVersion}+${env.buildNumber}'; // 必须和符号表归档对得上 - options.tracesSampleRate = 0.0; // 首版不开性能追踪,见下 - options.sendDefaultPii = false; // 关键:默认不采集 IP / 请求头 / 用户信息 - options.beforeBreadcrumb = scrubBreadcrumb; // 见「脱敏」 - options.beforeSend = scrubEvent; - }, - appRunner: () => runApp(ProviderScope( - retry: (_, __) => null, - observers: [ErrorObserver()], // 见 12 - child: const ContiApp(), - )), -); -``` - -**`appRunner` 不是可选写法。** 传了它,Sentry 会自己接管 `FlutterError.onError` 和 `PlatformDispatcher.instance.onError` 并把 `runApp` 放进受保护的 error zone;**这时候再手写一遍这两个回调,结果是同一个异常上报两次**,线上崩溃数直接翻倍,是这个 SDK 最常见的接入错误。 - -业务代码仍然只依赖 `core_logging` 暴露的 `CrashReporter` 接口,不直接 import `sentry_flutter`: - -```dart -// packages/core_logging/lib/src/crash_reporter.dart -abstract interface class CrashReporter { - void setUser(String userId); - void setTag(String key, String value); - void leaveBreadcrumb(String message); - void report(Object error, StackTrace? stack, {Map extra = const {}}); -} -``` - -这一层不是为了"将来可能换 Sentry"——**是为了测试里能直接 mock 掉,不必真的初始化 SDK**,以及让 `feature_*` 不多一条对三方 SDK 的直接依赖(见 [01-project-structure.md](./01-project-structure.md) 的依赖规则)。 - -### 符号表:唯一必须打通的一步 - -`sentry_dart_plugin` 包装 `sentry-cli`,构建后一条命令把 Dart 符号表、Android mapping、iOS dSYM 一起传上去: - -```yaml -# pubspec.yaml -sentry: - upload_debug_symbols: true - upload_source_maps: false # 不做 Web - project: conti-retail-app - org: continental - # auth_token 走 CI 环境变量 SENTRY_AUTH_TOKEN,不写进仓库 -``` - -```bash -# CI:build 之后立刻跑,见 08 -fvm flutter build appbundle --flavor prod --target lib/main_prod.dart \ - --dart-define-from-file=env/prod.json \ - --obfuscate --split-debug-info=build/symbols/$CI_COMMIT_TAG -fvm dart run sentry_dart_plugin -``` - -三条硬约束: - -- **`options.release` 必须和上传符号表时的 release 严格一致**,对不上的表现是"符号表传上去了,堆栈还是混淆的"——这是接入 Sentry 最常见的坑,且后台不会报错。统一由 `versionName+versionCode` 生成(见 08 的版本号规则)。 -- **上传步骤必须在 CI 里、紧跟 build**,不能靠人工。漏传一次,那个版本的崩溃就永久读不出来。 -- **本地符号表归档照旧保留**(08 要求 ≥1 年)。Sentry 能自动还原之后它不再是唯一手段,但仍是 Sentry 服务出问题/数据过期时的兜底。 - -### 必须关掉的默认行为 - -Sentry 的默认配置面向公网 C 端产品,有几项在门店场景下不能开: - -| 项 | 结论 | -|---|---| -| `sendDefaultPii` | **false**。开了会自动带上 IP、请求头(含 `Authorization`)、用户信息 | -| Session Replay / 截图(`attachScreenshot`) | **关闭**。收银、经营分析页面上有金额和客户信息 | -| `attachViewHierarchy` | 关闭。控件树里会出现输入框内容 | -| `tracesSampleRate` | **0.0**,首版不开性能追踪。接口耗时后端已有(见 backend/08),开了只是多一份跨境流量 | -| HTTP 面包屑里的 URL | **必须脱敏**:H5 URL 的 query 里带着 `ticket`,原样进面包屑等于把 token 发出去,见「脱敏」一节 | - -### 用户与门店上下文 - -会话状态变化时同步(见 [11-store-context-and-session.md](./11-store-context-and-session.md)): - -```dart -CrashReporter.instance - ..setUser(session.user.userId) // 只传 ID,不传手机号/姓名 - ..setTag('storeId', '${session.store.storeId}') - ..setTag('roleCode', session.user.roleCode) - ..setTag('flavor', env.flavorName); -``` - -**`storeId` 一定要带。** 它能直接回答"这个崩溃是不是只发生在某几家门店"——门店设备型号和网络环境高度集中,很多崩溃是设备相关的,没有这个维度只能盲猜。 - -`traceId` 在网络相关的错误上报时作为自定义字段带上,这样一条崩溃能直接关联到后端 ELK 里的那次请求(见 [backend/08-observability.md](../../conti-backend/docs/08-observability.md))。 - -### 崩溃前的页面路径 - -崩溃报告里最有用的上下文之一是"崩之前用户在哪几个页面"。go_router 的 `observers` 挂一个 `NavigationObserver`(见 [04-routing.md](./04-routing.md)),把最近的路由变化写进环形缓冲,随崩溃一起上报: - -```dart -class NavigationObserver extends NavigatorObserver { - NavigationObserver(this._reporter); - final CrashReporter _reporter; - - @override - void didPush(Route route, Route? previous) => - _reporter.leaveBreadcrumb('nav: ${route.settings.name}'); -} -``` - -注意**记的是路由名不是完整 URL**——`/webview?target=X&ticket=...` 里带着票据(见脱敏一节)。同理再补三处业务关键节点:H5 启动/失败、门店切换、扫码。这三条链路最长、最容易出问题。 - -### 上报什么、不上报什么 - -- **上报**:未捕获的 Dart 异常、原生崩溃、ANR、Riverpod provider 抛出的异常(通过 `ErrorObserver`,即使 UI 已经优雅处理了——"用户看到了漂亮的错误页"和"不需要知道有多少人看到"是两回事,见 12)。 -- **不上报**:`RequestCancelledException`(用户正常退出页面)、`UnauthorizedException`(正常的登出流程)。这两类是业务流程的一部分,上报只会把真正的崩溃淹掉。 - -### 验证接入真的成功了 - -崩溃上报最常见的失败模式是**静默不上报**——数据没传上来,但你以为 App 很稳定。所以: - -- `core_logging` 暴露一个 `throwTestException()`,**只在 dev flavor 下可调**,每次发版前在 dev 上验证一遍 Android 和 iOS 都能在 Sentry 里看到。 -- **同时验证堆栈是不是可读的**——这一步比"能收到"更容易漏。用 `--obfuscate` 打一个 release 包、跑一遍 `sentry_dart_plugin`、再触发一次异常,确认后台显示的是 Dart 文件名行号而不是 `_x12`。`release` 对不上的话就是这个表现,见上文。 -- uat 环境跑一个迭代后,对一下"Sentry 上的错误数"和"埋点里的 `api_failed` 数量级",如果差得离谱说明有一侧漏了。 - -## 二、日志规范 - -### `AppLogger` - -```dart -// packages/core_logging/lib/src/app_logger.dart -abstract interface class AppLogger { - void d(String message, {Map? data}); - void i(String message, {Map? data}); - void w(String message, {Object? error, StackTrace? stackTrace}); - void e(String message, {Object? error, StackTrace? stackTrace}); -} -``` - -各包**不直接用 `logger` 包,也不用 `print`/`debugPrint`**,统一注入 `AppLogger`。理由:将来换日志实现只改一处;同时 `print` 在 release 下不会被剥离,是一条实打实的信息泄漏通道。 - -### 级别与环境 - -| 环境 | 级别 | 输出 | -|---|---|---| -| dev | `debug` | 控制台,带颜色和调用栈 | -| uat | `info` | 控制台 + 内存环形缓冲(最近 500 条) | -| prod | `warning` | **不输出到控制台**,只进内存环形缓冲 + 随崩溃上报 | - -**prod 不打控制台日志**:Android 上 `logcat` 是全局可读的,任何装了 adb 或第三方日志 App 的人都能看到。门店设备上这不是理论风险。 - -**内存环形缓冲**的作用是:崩溃时把最近 N 条日志一起传上去,相当于一个"黑匣子"。不落磁盘,App 退出即消失,避免日志文件成为新的泄漏面。实现上挂在 `beforeSend` 里作为 `contexts` 附加,单个事件体积有上限,所以实际带的是**最近 30 条**,不是全部 500 条。 - -### 脱敏(PRD §21.3「敏感字段脱敏」) - -```dart -// packages/core_logging/lib/src/scrubber.dart -const _sensitiveKeys = { - 'token', 'accessToken', 'refreshToken', 'ticket', 'password', - 'code', // 短信验证码 - 'phone', 'mobile', 'idCard', 'bankCard', -}; - -String maskPhone(String v) => v.length >= 11 ? '${v.substring(0, 3)}****${v.substring(7)}' : '***'; -``` - -规则: - -- **请求/响应体不整体打日志**。只打 method、path、状态码、耗时、`code`、`traceId`。真要看 body 只在 dev 下开,且过一遍脱敏器。 -- **`Authorization` 头永远不打**,一个字符都不打——打前 8 位也不行,那既足够辅助暴力破解,也足够在日志里认出是谁的 token。 -- **H5 URL 打日志前必须去掉 query**:URL 里带着 `ticket`,整条打出去等于打 token。 -- 崩溃上报前再做一遍同样的脱敏——环形缓冲里的日志会随崩溃一起传上去。 - -**同一个脱敏器要挂到 Sentry 的两个钩子上**,这是上文 `SentryFlutter.init` 里 `beforeBreadcrumb` / `beforeSend` 的实现: - -```dart -// packages/core_logging/lib/src/sentry_scrubber.dart -Breadcrumb? scrubBreadcrumb(Breadcrumb? crumb, Hint hint) { - if (crumb == null) return null; - // SDK 自动记录的 HTTP 面包屑里 url 是完整的,query 里可能带 ticket / token - final url = crumb.data?['url']; - if (url is String) { - final u = Uri.tryParse(url); - crumb.data?['url'] = u == null ? '' : u.replace(query: '').toString(); - } - return crumb; -} -``` - -**`beforeBreadcrumb` 不能漏。** Sentry 默认会自动记录所有 HTTP 请求作为面包屑,我们在 05/10 里辛苦保证的"URL 不落日志",会被这条默认行为绕过去——它不走我们的 `AppLogger`。 - -### 不采集什么 - -出于合规(个人信息保护法「最小必要」原则)和 PRD §21.3: - -| 项 | 结论 | -|---|---| -| 崩溃截图 / Session Replay / View Hierarchy | **关闭**。收银、经营分析页面上有金额和客户信息 | -| 精确位置 | 不采集。App 没有需要精确位置的功能 | -| IMEI / IDFA / MAC / AndroidID | **不采集**(见 05,`X-Device-Id` 用的是匿名安装 UUID)。**神策原生 SDK 默认会采集设备标识来生成 `distinct_id`,必须在初始化时逐项关掉**;Sentry 侧靠 `sendDefaultPii = false` | -| 通讯录、短信 | 不申请权限 | -| 用户输入的原文 | 不打日志(包括搜索关键词里可能出现的车牌、手机号) | - -这份清单要和 App 的隐私政策(PRD §10.3,由 App Backend 下发)**逐条对齐**——隐私政策里没写的,代码里就不能采。**第三方 SDK 的默认采集行为是最容易在合规审查时出问题的地方**:神策和 Sentry 的隐私说明都要单独过一遍,并且要在**用户同意隐私政策之前不初始化**(两个 SDK 都支持延迟初始化),否则「同意前不采集」这条硬要求就破了。 - -## 三、埋点:以后端为主 - -**大部分业务埋点由后端从自己的请求日志和审计日志里出,客户端不重复做一遍。** - -理由很直接:任何一个业务动作(登录、切店、下单、入库、打开 H5)都会打到 App Backend 的接口上,后端已经有 `traceId`、用户上下文、门店上下文和 `@Audited` 审计通道(见 [backend/08-observability.md](../../conti-backend/docs/08-observability.md))。客户端再报一遍,得到的是同一件事的两份数据——而且客户端那份还更不可靠(可能丢、可能延迟、可能被篡改)。 - -**这两份数据最好落到同一个地方。** 神策有服务端 SDK / 数据导入接口,后端把业务事件写进同一个神策项目的话,运营就能做「扫码失败的门店,后续下单转化率是不是更低」这种跨端漏斗;分成两套系统也能跑,但每次跨端分析都要人工对数。**这一条要和后端确认**,见待确认项。 - -### Metabase 不是神策的替代品 - -后端侧提到过 Metabase。**它和神策不冲突,也不是二选一**——两者根本不在一层: - -| | 神策 | Metabase | -|---|---|---| -| 客户端采集 SDK | **有** | **没有**,它不采集任何数据 | -| 数据来源 | 自己的 SDK / 服务端导入 | 接已有的数据库、数仓 | -| 定位 | 采集 + 管道 + 分析平台 | BI / 看板层 | - -Metabase 官网自己把 Mixpanel、PostHog、Amplitude 列为**上游集成**——由那些工具负责采集,Metabase 在导出的数据上出图。这就说明了它的位置。 - -所以合理的分工是:**神策收客户端事件;后端的业务埋点本来就在自己库里,Metabase 接上去出报表。** 后端如果已经在用 Metabase,那是个好消息而不是冲突信号——它意味着上面「两份数据落到一个地方」这条有了第二种解法:不把业务数据推进神策,而是反过来把神策的客户端事件导出到同一个库,用 Metabase 统一出图,还能直接和订单、门店主数据 join。哪一种更合适取决于后端的数仓现状,一并列进待确认项。 - -### 分工 - -| PRD §22.1 事件 | 谁来出 | 说明 | -|---|---|---| -| 登录成功/失败 | **后端** | 登录本身就是接口调用 | -| 首页曝光 | **后端** | 首页聚合接口的调用即曝光 | -| 门店切换 | **后端** | 切换接口 | -| 采购下单 | **后端** | | -| 入库成功 | **后端** | | -| 待办点击 | **后端** | 点击后会请求详情接口 | -| **扫码成功/失败** | **客户端** | 扫码是 App 原生实现(见 07),**不产生任何请求**,后端完全看不到 | -| **H5 关闭 / 异常** | **客户端** | 「打开」有 `/h5/launch` 请求后端能看到;**关闭、白屏、超时、加载失败后端看不到** | -| 客服点击 | **客户端** | 拨号、企微二维码是纯客户端行为 | - -**客户端埋点的判据只有一条:这件事会不会产生一次后端请求?不会,才由客户端上报。** - -### 客户端事件表 - -按上面的判据筛下来,客户端只需要这几个: - -| 事件 | 触发 | 关键参数 | -|---|---|---| -| `scan_succeeded` / `scan_failed` | 扫码结果 | `mode`(barcode/vin/plate)、`durationMs`、`failReason` | -| `h5_closed` | H5 页关闭 | `target`、`stayDurationMs` | -| `h5_failed` | 白屏 / 超时 / 加载失败 | `target`、`errorCode`、`elapsedMs`、`traceId` | -| `h5_first_paint` | H5 首屏完成 | `target`、`ticketMs`(换票耗时)、`loadMs`(页面加载耗时) | -| `support_clicked` | 客服入口点击 | `channel`(hotline/dealer/o2o) | -| `api_failed` | 请求失败 | `path`、`code`、`httpStatus`、`traceId` | -| `app_cold_start` | 冷启动完成 | `durationMs` | -| `logout` | 登出 | `reason`(userInitiated / tokenExpired / sessionRevoked)。**被动登出没有对应的接口调用**,见 [11](./11-store-context-and-session.md) | -| `session_restore_failed` | 冷启动恢复会话失败 | 失败阶段(读 storage / me / stores)。卡在读 secure storage 时不产生任何网络请求 | - -两条说明: - -- **`h5_first_paint` 必须把耗时拆成 `ticketMs` 和 `loadMs` 两段**。合成一个数字的话,慢了不知道该找 App Backend / F6 / 还是网络——这是这个 App 里最长的一条跨系统链路,也是最容易互相甩锅的地方。 -- **`api_failed` 客户端也要报**,虽然后端也能看到失败。因为**后端看不到"请求根本没发出去"和"响应没收到"**:超时、连接失败、DNS 失败、运营商劫持,这些在后端日志里要么完全没有记录,要么表现为一次正常的成功响应。门店网络不稳时这类失败占大头。 - -### 实现约定:神策 SDK,外面包一层 - -客户端埋点走**神策 `sensors_analytics_flutter_plugin`**:官方 verified publisher `sensorsdata.cn`,`4.2.3` 一个多月前发布,是当前维护中的官方插件——这在 pub.dev 上的国内三方 SDK 里不多见(对比 Bugly 那两个 unverified 社区插件)。**团队过往项目用过,事件模型和数据接入的坑踩过一遍**,这是选它最实在的理由。 - -#### 神策不是"开箱即用",这些活一样要干 - -先把预期摆正,否则排期一定会低估。**接了神策之后,下面这些工作量和自建一套上报是完全一样的**: - -- **事件方案设计**——事件名、属性、口径对齐。这才是埋点的大头,跟用什么 SDK 无关。 -- **`core_analytics` 的接口封装**(见下)。 -- **接入点的编排**——超级属性什么时候注册、切店后重注册、登录/登出的 ID 关联。神策给了 API,但在哪调是我们的事(见 [11-store-context-and-session.md](./11-store-context-and-session.md) 的级联清单)。 -- **私有化部署的运维**(如果走私有化)。 - -**神策真正替我们省掉的只有一件具体的事:客户端的可靠投递。** 原生 SDK 自带本地缓存、批量上报、弱网重传、后台 flush 和进程被杀后的补发。门店网络不稳,这个模块不能省,自己写的话是**容易写得看起来对、实际在丢数据**的那一类——丢了还不会有人发现。这一条就是选现成 SDK 的全部收益,其余都要照做。 - -#### 依赖与封装 - -```yaml -dependencies: - sensors_analytics_flutter_plugin: ^4.2.3 -``` - -业务代码仍然只见 `core_analytics` 的接口,不直接 import 神策: - -```dart -// packages/core_analytics/lib/src/analytics.dart -abstract interface class Analytics { - void track(String event, [Map params = const {}]); - void registerSuperProperties(Map props); // 公共属性,注册一次全局附加 - void identify(String userId); // 登录成功后调 - void reset(); // 登出时调 -} -``` - -理由和 `CrashReporter` 一样:测试里能 mock,`feature_*` 不多一条对三方 SDK 的直接依赖。**另外它也是采购未落地时的缓冲**——接口先定、事件方案先做,实现类换成一个最小的 `POST /api/v1/events/batch` 也只改一个文件(代价就是上面那条可靠投递要自己补)。 - -接入约定: - -- **公共属性用「超级属性」注册一次,不在每个调用点手写**:`storeId`、`roleCode`、`flavor`、`appVersion`、`buildNumber`。`storeId` 尤其重要——运营侧几乎所有分析都按门店维度看,靠每个调用点自己传一定会漏。**门店切换后必须重新注册**(见 [11-store-context-and-session.md](./11-store-context-and-session.md) 的级联清单)。 -- **登录/登出走 `login()` / `logout()`**:登录成功后用后端的 `userId` 关联匿名 ID,登出时断开,否则同一台设备上换人登录的数据会串到一起(门店设备是共用的,这个场景一定会发生)。 -- **埋点失败绝不能影响业务**:`track()` 内部 try-catch 兜住,任何异常只记日志不外抛。 -- **dev/uat 与 prod 必须分开**——独立项目,或至少用不同的数据接收地址。共用一个项目的话,测试数据会直接污染运营报表,且事后无法剔除。 - -#### 全埋点(AutoTrack):只开启动/退出,其余关掉 - -神策的全埋点支持 `APP_START` / `APP_END` / `APP_CLICK` / `APP_VIEW_SCREEN` 四类。我们的结论: - -| 类型 | 结论 | -|---|---| -| `APP_START` / `APP_END` | **开**。启动次数、使用时长是零成本拿到的基础指标 | -| `APP_CLICK` | **关**。Flutter 的控件树没有原生 `id`/`resource-name`,采上来的元素标识基本不可读,是纯噪音 | -| `APP_VIEW_SCREEN` | **关**。改用我们自己的 `NavigationObserver` 上报路由名——既更准,也**避免把 `/webview?target=X&ticket=...` 整条 URL 采上去**(见脱敏一节) | - -“少采一点”在这里不是保守,是因为**采上来读不懂的数据比没有更糟**:它会让报表看起来有数据,实际没法用。 - -#### H5 内部的埋点不归我们 - -F6 的 H5 页面是外部系统,页面内部的行为埋点由 F6 自己负责。**客户端只报容器级事件**(打开/关闭/失败/首屏耗时),不往 WebView 里注入神策的 JS SDK——注进去就等于我们要为别人页面里的数据质量负责,而且 JSBridge 的能力清单([10-webview-h5.md](./10-webview-h5.md) 的 12 项)里也没有埋点这一项。 - -### 命名约定 - -`snake_case`,`对象_动作` 或 `对象_动作_结果`。结果类用过去式(`succeeded`/`failed`),动作类用现在式(`clicked`)。 - -事件名和参数名一旦上线**不再改**——改名意味着历史数据断裂,运营报表要重做。要加维度就加参数。事件名统一定义为常量(`AnalyticsEvent.scanSucceeded`),不允许在调用处写字符串字面量:拼写错误编译期发现不了,在报表里表现为"这个事件怎么没数据"。 - -## 四、性能指标 - -| 指标 | 怎么测 | 目标 | -|---|---|---| -| 冷启动到首帧 | `WidgetsBinding.instance.addTimingsCallback` | < 2s | -| 冷启动到首页可用 | `main()` → 首页数据渲染完成 | < 3s | -| H5 打开耗时 | `h5_first_paint` 的 `ticketMs + loadMs` | < 3s(PRD §21.1 要求有超时策略) | -| 接口耗时 | 后端侧统计即可,客户端不重复报 | P95 < 1s | -| 帧率 | 先不做自动采集,用 DevTools 人工测关键页面 | — | - -**接口耗时不由客户端报**:后端有完整的请求日志和 Micrometer 指标(见 backend/08)。客户端唯一能补充的是"客户端观测到的耗时 - 服务端处理耗时 = 网络耗时",这个差值有价值但不是首版必须,先不做。 - -## 五、和后端审计日志的分工 - -`backend/08-observability.md` 已经有 `@Audited` 审计日志通道。**审计以服务端为准,客户端不做审计**——客户端日志可被篡改,不能作为审计依据。 - -| | 客户端埋点 | 服务端日志/审计 | -|---|---|---| -| 目的 | 补齐后端看不到的行为 | 业务分析、合规追溯 | -| 可信度 | 参考 | 权威 | -| 覆盖 | 纯客户端行为、请求失败 | 所有到达服务端的操作 | - -## 待确认项 - -- **Sentry 是自建还是用 SaaS**——这是本篇最硬的阻塞项,决定可达性和数据出境合规口径。需要明确内网资源、运维承接方、法务意见。见上文「唯一还没定的」。 -- Sentry 的 org/project 划分:dev/uat/prod 是三个 project 还是靠 `environment` 区分(建议 prod 单独一个 project,避免测试数据污染线上崩溃率告警)。 -- **公司有没有在用的神策服务?** 本项目没有现成账号。有的话拿数据接收地址即可;**没有的话开通神策是采购流程,不是配置项**,周期可能比开发长。这一项是埋点唯一的外部依赖——**但它不阻塞开工**:事件方案设计和 `core_analytics` 接口先做,这两块工作量与最终用什么 SDK 无关。真的走不通,实现类换成最小的 `POST /api/v1/events/batch`,代价是可靠投递要自己补。 -- 若确认用神策:数据接收地址是私有化部署还是神策云,以及 dev/uat/prod 的项目划分。地址走 `--dart-define-from-file`,代码里不写死。 -- **客户端事件和后端业务数据怎么汇到一起**:是后端用神策服务端 SDK 写进同一个神策项目,还是把神策的客户端事件导出到后端数仓、统一用 Metabase 出图。取决于后端数仓现状和 Metabase 的实际使用情况,要和后端一起定。 -- 神策原生 SDK 的默认设备信息采集项(`distinct_id` 的生成方式、是否取 AndroidID/IDFA),需逐项关闭并与隐私政策对齐(法务侧)。 -- **后端的业务埋点写不写进同一个神策项目**(用神策服务端 SDK / 数据导入),还是留在自己的 ELK 里出报表。影响的是能不能做跨端漏斗分析。 -- 后端从请求日志出业务埋点的具体口径(哪个接口对应哪个事件),需要和后端一起把 PRD §22.1 的 9 类事件逐条落到接口上。 -- 神策和 Sentry 都要在**用户同意隐私政策之后**才初始化,具体的延迟初始化时机要和 `feature_auth` 的协议弹窗流程对齐。 -- 性能指标目标值需在真机(门店常用的中低端 Android)实测后校准,上表是初始预期值。 - -## 参考链接 - -- [sentry_flutter | Dart package](https://pub.dev/packages/sentry_flutter) -- [sentry_dart_plugin | Dart package](https://pub.dev/packages/sentry_dart_plugin)(上传 Dart 符号表 / mapping / dSYM) -- [Sentry: Flutter Debug Symbols](https://docs.sentry.io/platforms/dart/guides/flutter/debug-symbols/) -- [Sentry: 自建(self-hosted)](https://develop.sentry.dev/self-hosted/) -- [sensors_analytics_flutter_plugin | Dart package](https://pub.dev/packages/sensors_analytics_flutter_plugin) -- [神策:Flutter 插件集成文档](https://manual.sensorsdata.cn/sa/docs/tech_sdk_client_flutter_plugin/v0300) -- [神策:Flutter 全埋点](https://manual.sensorsdata.cn/sa/docs/tech_sdk_client_flutter_auto_track/v0205) -- [Metabase](https://www.metabase.com/)(BI 层,非采集方案;官网把 Mixpanel/PostHog/Amplitude 列为上游采集集成) -- [Flutter: 混淆与 `flutter symbolize`](https://docs.flutter.dev/deployment/obfuscate) -- [logger | Dart package](https://pub.dev/packages/logger) -- [backend/08-observability.md:traceId 与审计日志](../../conti-backend/docs/08-observability.md) -- [PRD §21.4 可观测性 / §22.1 埋点](../../conti-docs/Architecture-Diagram/202606-Continental-Retail-APP-PRD.md) +如未来需要崩溃上报,须单独完成合规、数据字段、脱敏、符号表保存和 SDK 选型评审;不得 +直接恢复本项目已删除的第三方 SDK 配置。 diff --git a/packages/core_analytics/lib/src/analytics.dart b/packages/core_analytics/lib/src/analytics.dart index b87f3e9..6f14684 100644 --- a/packages/core_analytics/lib/src/analytics.dart +++ b/packages/core_analytics/lib/src/analytics.dart @@ -3,7 +3,7 @@ library; /// 业务代码唯一可见的埋点接口。 /// -/// 理由和 `CrashReporter` 一样:测试里能 mock,`feature_*` 不多一条对三方 +/// 这样测试里能 mock,`feature_*` 不多一条对三方 /// SDK 的直接依赖。**另外它也是采购未落地时的缓冲**——接口先定、事件方案 /// 先做,实现类换成一个最小的 `POST /api/v1/events/batch` 也只改一个文件。 /// diff --git a/packages/core_auth/lib/src/session_ports.dart b/packages/core_auth/lib/src/session_ports.dart index dd0515a..fe57c04 100644 --- a/packages/core_auth/lib/src/session_ports.dart +++ b/packages/core_auth/lib/src/session_ports.dart @@ -6,7 +6,7 @@ /// 11 的 `switchStore` / `logout` 伪代码直接 `ref.read` 了 /// `webViewSessionProvider`(core_webview)、`appDatabaseProvider`(core_storage)、 /// `prefsProvider`(core_storage)、`goRouterProvider`(core_router)、 -/// `crashReporterProvider`(core_logging)、`analyticsProvider`(core_analytics)。 +/// `analyticsProvider`(core_analytics)。 /// /// 但 01 的依赖约束里,`core_auth` 一条 `core_*` 出边都没有(连允许的三条例外 /// 都是别人指向它)。照抄伪代码会把 core_auth 变成整个 core 层的汇聚点, @@ -72,9 +72,8 @@ abstract interface class SessionScopedStore { /// 会话变化时需要同步的观测 / 埋点侧。 /// -/// 由 app 层用 core_logging 的 `CrashReporter` 和 core_analytics 的 `Analytics` -/// 实现。**漏了这一步的表现是切店后的崩溃和埋点还挂在旧门店名下**,按门店维度 -/// 分析时数据是错的,而且错得很隐蔽(11 步骤 8)。 +/// 由 app 层用 core_analytics 的 `Analytics` 实现。漏了这一步会让切店后的埋点 +/// 仍挂在旧门店名下,按门店维度分析时数据会悄悄出错。 abstract interface class SessionObserver { /// 门店上下文已更新。 void onStoreChanged(StoreContext store); diff --git a/packages/core_auth/test/core_auth_test.dart b/packages/core_auth/test/core_auth_test.dart index dd03a5f..b183b59 100644 --- a/packages/core_auth/test/core_auth_test.dart +++ b/packages/core_auth/test/core_auth_test.dart @@ -91,7 +91,6 @@ void main() { flavor: AppFlavor.dev, apiBaseUrl: 'https://example.test', enableLog: false, - sentryDsn: '', h5AllowedHosts: {}, ), ); diff --git a/packages/core_foundation/lib/src/env/app_env.dart b/packages/core_foundation/lib/src/env/app_env.dart index b4efcae..6591bb3 100644 --- a/packages/core_foundation/lib/src/env/app_env.dart +++ b/packages/core_foundation/lib/src/env/app_env.dart @@ -22,41 +22,36 @@ enum AppFlavor { /// **所有环境差异都收敛在这一个类里**,业务代码里不允许出现 /// `if (kDebugMode)` 或 `if (host.contains('uat'))` 这类判断。 /// -/// 值来自 `--dart-define-from-file=env/{flavor}.json`(见 08)。构建时不传这个 -/// 参数会得到空的 baseUrl,[fromDartDefine] 会直接抛错而不是让 App 带着空配置跑起来—— -/// 这种错误必须在启动瞬间暴露,而不是等第一个网络请求 404。 +/// 值来自随 App 打包的 `env/{flavor}.json`。配置无效时会在启动瞬间暴露, +/// 而不是等第一个网络请求 404。 class AppEnv { const AppEnv({ required this.flavor, required this.apiBaseUrl, required this.enableLog, - required this.sentryDsn, required this.h5AllowedHosts, }); - /// 从 `--dart-define-from-file` 注入的值构造。 - /// - /// [flavor] 由各自的入口文件(main_dev.dart / main_uat.dart / main_prod.dart) - /// 硬编码传入,而不是从 dart-define 读——这样"用 dev 的入口配了 prod 的 json" - /// 这种事故在代码里看得见。 - factory AppEnv.fromDartDefine({required String flavor}) { - const apiBaseUrl = String.fromEnvironment('API_BASE_URL'); - const enableLog = bool.fromEnvironment('ENABLE_LOG'); - const sentryDsn = String.fromEnvironment('SENTRY_DSN'); - const h5AllowedHosts = String.fromEnvironment('H5_ALLOWED_HOSTS'); + /// 从已解析的环境 JSON 构造,并在启动期验证所有必填字段。 + factory AppEnv.fromConfig({required String flavor, required Map values}) { + final Object? apiBaseUrl = values['API_BASE_URL']; + final Object? enableLog = values['ENABLE_LOG']; + final Object? h5AllowedHosts = values['H5_ALLOWED_HOSTS']; - if (apiBaseUrl.isEmpty) { - throw StateError( - 'API_BASE_URL 为空。启动时必须带上 --dart-define-from-file=env/$flavor.json,' - '见 README「怎么跑起来」。', - ); + if (apiBaseUrl is! String || apiBaseUrl.trim().isEmpty) { + throw StateError('环境 $flavor 的 API_BASE_URL 必须是非空字符串。'); + } + if (enableLog is! bool) { + throw StateError('环境 $flavor 的 ENABLE_LOG 必须是布尔值。'); + } + if (h5AllowedHosts is! String) { + throw StateError('环境 $flavor 的 H5_ALLOWED_HOSTS 必须是字符串。'); } return AppEnv( flavor: AppFlavor.parse(flavor), apiBaseUrl: apiBaseUrl, enableLog: enableLog, - sentryDsn: sentryDsn, h5AllowedHosts: _parseHosts(h5AllowedHosts), ); } @@ -75,9 +70,6 @@ class AppEnv { /// 是否输出网络日志和 debug 级日志。prod 必须为 false(见 13)。 final bool enableLog; - /// 为空表示不启用 Sentry(dev 默认不上报,避免把开发期噪音混进线上数据)。 - final String sentryDsn; - /// H5 域名白名单。WebView 只允许加载这些域名及其子域,见 10。 final Set h5AllowedHosts; diff --git a/packages/core_foundation/test/app_exception_test.dart b/packages/core_foundation/test/app_exception_test.dart index d2e4f4d..e762428 100644 --- a/packages/core_foundation/test/app_exception_test.dart +++ b/packages/core_foundation/test/app_exception_test.dart @@ -44,12 +44,48 @@ void main() { flavor: AppFlavor.dev, apiBaseUrl: 'https://api-dev.conti.com', enableLog: true, - sentryDsn: '', h5AllowedHosts: {'f6.conti.com'}, ); expect(env.isProd, isFalse); expect(env.flavorSuffixVisible, isTrue); expect(env.h5AllowedHosts, contains('f6.conti.com')); }); + + test('从内置环境配置读取字段并规范化 H5 域名', () { + final AppEnv env = AppEnv.fromConfig( + flavor: 'uat', + values: { + 'API_BASE_URL': 'https://api-uat.conti.com', + 'ENABLE_LOG': true, + 'H5_ALLOWED_HOSTS': ' H5.Conti.com, shop.conti.com ', + }, + ); + + expect(env.flavor, AppFlavor.uat); + expect(env.apiBaseUrl, 'https://api-uat.conti.com'); + expect(env.enableLog, isTrue); + expect(env.h5AllowedHosts, {'h5.conti.com', 'shop.conti.com'}); + }); + + test('缺失或类型错误的内置配置在启动期失败', () { + expect( + () => AppEnv.fromConfig( + flavor: 'dev', + values: {'API_BASE_URL': '', 'ENABLE_LOG': true, 'H5_ALLOWED_HOSTS': ''}, + ), + throwsStateError, + ); + expect( + () => AppEnv.fromConfig( + flavor: 'dev', + values: { + 'API_BASE_URL': 'https://api-dev.conti.com', + 'ENABLE_LOG': 'true', + 'H5_ALLOWED_HOSTS': '', + }, + ), + throwsStateError, + ); + }); }); } diff --git a/packages/core_logging/lib/core_logging.dart b/packages/core_logging/lib/core_logging.dart index 1e4053d..093df1f 100644 --- a/packages/core_logging/lib/core_logging.dart +++ b/packages/core_logging/lib/core_logging.dart @@ -1,19 +1,11 @@ -/// 日志与崩溃上报的统一入口。来源:conti-docs/13-observability-analytics.md。 +/// 日志与敏感信息脱敏的统一入口。 /// /// 边界约定: -/// - 业务代码只用 [AppLogger] / [CrashReporter] 两个接口,**不直接 import -/// `logger` 或 `sentry_flutter`**,也不用 `print` / `debugPrint`; -/// - 全仓库只有本包的 `sentry_*.dart` 和 `app/bootstrap.dart` 可以见到 -/// Sentry SDK。 +/// - 业务代码只用 [AppLogger],不直接 import `logger`,也不用 `print` / +/// `debugPrint`。 library; export 'src/app_logger.dart'; -export 'src/crash_breadcrumb_observer.dart'; -export 'src/crash_reporter.dart'; -export 'src/log_buffer.dart'; export 'src/logger_app_logger.dart'; export 'src/providers.dart'; export 'src/scrubber.dart'; -export 'src/sentry_crash_reporter.dart'; -export 'src/sentry_scrubber.dart'; -export 'src/test_exception.dart'; diff --git a/packages/core_logging/lib/src/crash_breadcrumb_observer.dart b/packages/core_logging/lib/src/crash_breadcrumb_observer.dart deleted file mode 100644 index df849c6..0000000 --- a/packages/core_logging/lib/src/crash_breadcrumb_observer.dart +++ /dev/null @@ -1,38 +0,0 @@ -/// 路由面包屑观察者。来源:13 §一「崩溃前的页面路径」。 -library; - -import 'package:flutter/widgets.dart'; - -import 'crash_reporter.dart'; - -/// 把路由变化写进 Sentry 面包屑。 -/// -/// **记的是路由名不是完整 URL**——`/webview?target=X&ticket=...` 里带着票据 -/// (见 10 与本包的 `scrubber.dart`)。 -/// -/// 挂载位置:`app/` 在构造 GoRouter 时通过 `navigatorObserversProvider` -/// 注入。`core_router` 不依赖 `core_logging`(那不在 01 允许的三条 core 间 -/// 依赖里),所以这个类住在这里而不是路由包。详见 SCAFFOLD-NOTES.md §G。 -class CrashBreadcrumbObserver extends NavigatorObserver { - /// [reporter] 通常是 `SentryCrashReporter`。 - CrashBreadcrumbObserver(this._reporter); - - final CrashReporter _reporter; - - @override - void didPush(Route route, Route? previousRoute) => _leave('push', route); - - @override - void didPop(Route route, Route? previousRoute) => _leave('pop', route); - - @override - void didReplace({Route? newRoute, Route? oldRoute}) { - if (newRoute != null) _leave('replace', newRoute); - } - - void _leave(String action, Route route) { - // name 为空时用 '',绝不回退到 route.settings.arguments 或 - // 完整 location——那两个都可能带业务参数。 - _reporter.leaveBreadcrumb('nav: $action ${route.settings.name ?? ''}'); - } -} diff --git a/packages/core_logging/lib/src/crash_reporter.dart b/packages/core_logging/lib/src/crash_reporter.dart deleted file mode 100644 index 495eec4..0000000 --- a/packages/core_logging/lib/src/crash_reporter.dart +++ /dev/null @@ -1,56 +0,0 @@ -/// 崩溃上报门面。来源:13 §一。 -library; - -/// 业务代码唯一可见的崩溃上报接口。 -/// -/// 这一层**不是**为了"将来可能换 Sentry"——是为了测试里能直接 mock 掉、 -/// 不必真的初始化 SDK,以及让 `feature_*` 不多一条对三方 SDK 的直接依赖 -/// (见 01 的依赖规则)。 -abstract interface class CrashReporter { - /// 只传后端的 `userId`,**不传手机号 / 姓名**(13 §一「用户与门店上下文」)。 - void setUser(String userId); - - /// 设置一个自定义维度。`storeId` 一定要带——门店设备型号和网络环境高度 - /// 集中,很多崩溃是设备相关的,没有这个维度只能盲猜。 - void setTag(String key, String value); - - /// 清空用户与门店维度。登出时调用,否则共用设备上会串号。 - void clearUser(); - - /// 面包屑。记路由名而不是完整 URL——URL 的 query 里带着票据。 - void leaveBreadcrumb(String message); - - /// 上报一个异常。 - /// - /// [extra] 用于带 `traceId` 这类能直接关联到后端 ELK 的字段。 - void report( - Object error, - StackTrace? stack, { - Map extra = const {}, - }); -} - -/// 测试与未接入环境用的空实现。 -class NoopCrashReporter implements CrashReporter { - /// 创建一个什么都不做的上报器。 - const NoopCrashReporter(); - - @override - void setUser(String userId) {} - - @override - void setTag(String key, String value) {} - - @override - void clearUser() {} - - @override - void leaveBreadcrumb(String message) {} - - @override - void report( - Object error, - StackTrace? stack, { - Map extra = const {}, - }) {} -} diff --git a/packages/core_logging/lib/src/log_buffer.dart b/packages/core_logging/lib/src/log_buffer.dart deleted file mode 100644 index 3ac4c22..0000000 --- a/packages/core_logging/lib/src/log_buffer.dart +++ /dev/null @@ -1,57 +0,0 @@ -/// 内存环形缓冲——崩溃时的"黑匣子"。来源:13 §二「级别与环境」。 -library; - -/// 一条已经脱敏、可以直接外发的日志。 -class LogRecord { - /// 构造一条日志记录。[message] 必须是脱敏之后的内容。 - const LogRecord({required this.level, required this.message, required this.timestamp}); - - /// 级别缩写:`D` / `I` / `W` / `E`。 - final String level; - - /// 已脱敏的消息体。 - final String message; - - /// 记录时刻(本地时区)。 - final DateTime timestamp; - - @override - String toString() => '${timestamp.toIso8601String()} [$level] $message'; -} - -/// 固定容量的环形缓冲。 -/// -/// 只在内存里,**不落磁盘**——日志文件会成为新的泄漏面,且门店设备上 -/// `logcat` 与外部存储都不是可信边界。App 退出即消失。 -class LogBuffer { - /// [capacity] 默认 500,与 13 §二的表格一致。 - LogBuffer({this.capacity = 500}) : assert(capacity > 0, 'capacity 必须为正'); - - /// 最多保留的条数。 - final int capacity; - - final List _records = []; - - /// 追加一条,超出容量时丢弃最旧的。 - void add(LogRecord record) { - _records.add(record); - if (_records.length > capacity) { - _records.removeRange(0, _records.length - capacity); - } - } - - /// 取最近 [count] 条,按时间正序。 - /// - /// 崩溃上报时实际带走的是 30 条而不是全部 500 条——单个 Sentry 事件的 - /// 体积有上限,超了整条事件会被丢弃,那比少带日志更糟。 - List recent([int count = 30]) { - if (_records.length <= count) return List.unmodifiable(_records); - return List.unmodifiable(_records.sublist(_records.length - count)); - } - - /// 当前条数。 - int get length => _records.length; - - /// 清空。登出时调用,避免上一个账号的日志被下一个账号的崩溃带走。 - void clear() => _records.clear(); -} diff --git a/packages/core_logging/lib/src/logger_app_logger.dart b/packages/core_logging/lib/src/logger_app_logger.dart index 3fab85e..0250628 100644 --- a/packages/core_logging/lib/src/logger_app_logger.dart +++ b/packages/core_logging/lib/src/logger_app_logger.dart @@ -5,7 +5,6 @@ import 'package:core_foundation/core_foundation.dart'; import 'package:logger/logger.dart'; import 'app_logger.dart'; -import 'log_buffer.dart'; import 'scrubber.dart'; /// 日志级别,按严重程度递增。 @@ -33,14 +32,12 @@ enum LogSeverity { /// 三条硬约束(13 §二): /// 1. **prod 不输出到控制台**——Android 的 `logcat` 是全局可读的,门店设备上 /// 这不是理论风险; -/// 2. 所有内容进环形缓冲前**必须过一遍脱敏器**,因为缓冲会随崩溃外发; +/// 2. 结构化日志输出前**必须过一遍脱敏器**; /// 3. 级别按 flavor 分档:dev=debug / uat=info / prod=warning。 class LoggerAppLogger implements AppLogger { /// 按 [env] 推导级别和控制台开关。 /// - /// [buffer] 由调用方持有并同时交给 `CrashReporter`,两边必须是同一个实例, - /// 否则崩溃上报里带的是一份空日志。 - LoggerAppLogger({required AppEnv env, required this.buffer, Logger? logger}) + LoggerAppLogger({required AppEnv env, Logger? logger}) : _minSeverity = _severityFor(env.flavor), // enableLog 是 dart-define 里的开关,prod 即使误开也不打控制台。 _console = env.enableLog && !env.isProd, @@ -64,9 +61,6 @@ class LoggerAppLogger implements AppLogger { AppFlavor.prod => LogSeverity.warning, }; - /// 崩溃时随事件外发的"黑匣子"。与 `beforeSend` 读的必须是同一个实例。 - final LogBuffer buffer; - final LogSeverity _minSeverity; final bool _console; final Logger _logger; @@ -98,17 +92,6 @@ class LoggerAppLogger implements AppLogger { final String line = data == null || data.isEmpty ? message : '$message ${scrubMap(data)}'; - buffer.add( - LogRecord( - level: severity.tag, - // error 的 toString 无法结构化脱敏,所以约定反过来:仓库内的异常类型 - // (见 core_foundation 的 AppException)只带 code / traceId,不带任何 - // 凭据或用户输入原文。三方异常同理,发现例外要在这里显式拦。 - message: error == null ? line : '$line | error=$error', - timestamp: DateTime.now(), - ), - ); - if (!_console) return; switch (severity) { case LogSeverity.debug: diff --git a/packages/core_logging/lib/src/providers.dart b/packages/core_logging/lib/src/providers.dart index 88810c0..8382f6f 100644 --- a/packages/core_logging/lib/src/providers.dart +++ b/packages/core_logging/lib/src/providers.dart @@ -4,14 +4,6 @@ library; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'app_logger.dart'; -import 'crash_reporter.dart'; -import 'log_buffer.dart'; - -/// 环形缓冲的唯一实例。 -/// -/// `AppLogger` 往里写、`beforeSend` 从里读,两边必须是同一个对象, -/// 否则崩溃上报里带的是一份空日志。 -final Provider logBufferProvider = Provider((Ref ref) => LogBuffer()); /// 全局日志入口。 /// @@ -21,8 +13,3 @@ final Provider logBufferProvider = Provider((Ref ref) => L final Provider appLoggerProvider = Provider( (Ref ref) => const NoopAppLogger(), ); - -/// 全局崩溃上报入口。默认空实现,同 [appLoggerProvider]。 -final Provider crashReporterProvider = Provider( - (Ref ref) => const NoopCrashReporter(), -); diff --git a/packages/core_logging/lib/src/scrubber.dart b/packages/core_logging/lib/src/scrubber.dart index 206f56c..c576bb3 100644 --- a/packages/core_logging/lib/src/scrubber.dart +++ b/packages/core_logging/lib/src/scrubber.dart @@ -1,7 +1,7 @@ -/// 脱敏器。来源:13 §二「脱敏」、PRD §21.3。 +/// 脱敏器。来源:PRD §21.3。 /// -/// 这个文件是全仓库唯一的脱敏实现,`AppLogger`、Sentry 的两个钩子、以及 -/// 网络层的日志拦截器都必须走这里,不允许各写各的。 +/// 这个文件是全仓库唯一的脱敏实现,`AppLogger` 与网络层的日志拦截器都必须 +/// 走这里,不允许各写各的。 library; /// 命中即整体替换为 [redacted] 的 key(大小写不敏感)。 diff --git a/packages/core_logging/lib/src/sentry_crash_reporter.dart b/packages/core_logging/lib/src/sentry_crash_reporter.dart deleted file mode 100644 index 706f464..0000000 --- a/packages/core_logging/lib/src/sentry_crash_reporter.dart +++ /dev/null @@ -1,71 +0,0 @@ -/// `CrashReporter` 的 Sentry 实现。来源:13 §一。 -/// -/// **本文件是全仓库唯一允许 `import 'package:sentry_flutter/...'` 的地方** -/// (另加 app/ 里的 `SentryFlutter.init`)。 -library; - -import 'dart:async'; - -import 'package:sentry_flutter/sentry_flutter.dart'; - -import 'crash_reporter.dart'; - -/// 把 [CrashReporter] 转接到 Sentry SDK。 -/// -/// 所有方法都是"发射后不管":上报本身绝不能阻塞或影响业务流程, -/// 因此统一 `unawaited` 并吞掉自身异常。 -class SentryCrashReporter implements CrashReporter { - /// 创建一个转接到全局 `Sentry` 的上报器。 - const SentryCrashReporter(); - - @override - void setUser(String userId) => _configure((Scope scope) async { - // 只传 ID。手机号、姓名、IP 一律不带(sendDefaultPii 也已关闭)。 - await scope.setUser(SentryUser(id: userId)); - }); - - @override - void setTag(String key, String value) => _configure((Scope scope) => scope.setTag(key, value)); - - @override - void clearUser() => _configure((Scope scope) async { - await scope.setUser(null); - await scope.removeTag('storeId'); - await scope.removeTag('roleCode'); - }); - - @override - void leaveBreadcrumb(String message) { - unawaited(Sentry.addBreadcrumb(Breadcrumb(message: message)).catchError((_) {})); - } - - @override - void report( - Object error, - StackTrace? stack, { - Map extra = const {}, - }) { - unawaited( - Sentry.captureException( - error, - stackTrace: stack, - withScope: (Scope scope) async { - if (extra.isNotEmpty) { - // traceId 放这里——一条崩溃能直接关联到后端 ELK 里的那次请求。 - await scope.setContexts('app_extra', extra); - } - }, - ).then((_) {}).catchError((_) {}), - ); - } - - void _configure(FutureOr Function(Scope) callback) { - unawaited(() async { - try { - await Sentry.configureScope(callback); - } on Object { - // 上报链路自身的异常不能外溢到业务流程。 - } - }()); - } -} diff --git a/packages/core_logging/lib/src/sentry_scrubber.dart b/packages/core_logging/lib/src/sentry_scrubber.dart deleted file mode 100644 index f42eae7..0000000 --- a/packages/core_logging/lib/src/sentry_scrubber.dart +++ /dev/null @@ -1,43 +0,0 @@ -/// Sentry 的两个脱敏钩子。来源:13 §二「脱敏」。 -/// -/// 这两个钩子是 `SentryFlutter.init` 里 `beforeBreadcrumb` / `beforeSend` -/// 的实现,**一个都不能漏**:Sentry 默认会自动记录所有 HTTP 请求作为面包屑, -/// 我们在 05/10 里辛苦保证的"URL 不落日志"会被这条默认行为绕过去—— -/// 它不走我们的 `AppLogger`。 -library; - -import 'package:sentry_flutter/sentry_flutter.dart'; - -import 'log_buffer.dart'; -import 'scrubber.dart'; - -/// 面包屑脱敏:剥掉 URL 的 query(里面可能带 `ticket` / `token`)。 -Breadcrumb? scrubBreadcrumb(Breadcrumb? crumb, Hint hint) { - if (crumb == null) return null; - final Object? url = crumb.data?['url']; - if (url is String) { - crumb.data?['url'] = scrubUrl(url); - } - return crumb; -} - -/// 事件脱敏 + 挂载"黑匣子"。 -/// -/// 返回的 `beforeSend` 闭包做两件事: -/// 1. 把环形缓冲里**最近 30 条**日志作为 context 附上——不是全部 500 条, -/// 单个事件有体积上限,超了整条事件会被丢弃; -/// 2. 剥掉请求 URL 的 query。 -BeforeSendCallback buildScrubEvent(LogBuffer buffer) { - return (SentryEvent event, Hint hint) { - final SentryRequest? request = event.request; - final String? url = request?.url; - if (request != null && url != null) { - request.url = scrubUrl(url); - } - - return event - ..contexts['app_logs'] = { - 'recent': buffer.recent().map((LogRecord r) => r.toString()).toList(growable: false), - }; - }; -} diff --git a/packages/core_logging/lib/src/test_exception.dart b/packages/core_logging/lib/src/test_exception.dart deleted file mode 100644 index d54e2fa..0000000 --- a/packages/core_logging/lib/src/test_exception.dart +++ /dev/null @@ -1,17 +0,0 @@ -/// 上报链路的自检入口。来源:13 §一「验证接入真的成功了」。 -library; - -import 'package:core_foundation/core_foundation.dart'; - -/// 崩溃上报最常见的失败模式是**静默不上报**——数据没传上来,但你以为 -/// App 很稳定。所以每次发版前在 dev 上调一次这个函数,确认 Android 和 iOS -/// 都能在 Sentry 里看到,**并且堆栈是可读的 Dart 文件名行号而不是 `_x12`**。 -/// -/// 只在 dev flavor 下可调;uat/prod 调用会直接抛 [StateError], -/// 这样误留在代码里的调用会在测试环境就暴露,而不是污染线上崩溃率。 -Never throwTestException(AppEnv env) { - if (env.flavor != AppFlavor.dev) { - throw StateError('throwTestException 只允许在 dev flavor 调用'); - } - throw Exception('Sentry 接入自检:这是一条人为触发的测试异常'); -} diff --git a/packages/core_logging/pubspec.yaml b/packages/core_logging/pubspec.yaml index c60bd1c..8cbea6d 100644 --- a/packages/core_logging/pubspec.yaml +++ b/packages/core_logging/pubspec.yaml @@ -1,5 +1,5 @@ name: core_logging -description: 日志与崩溃上报的统一入口(AppLogger / CrashReporter),含敏感信息脱敏。 +description: 应用日志与敏感信息脱敏。 publish_to: none version: 0.1.0 resolution: workspace @@ -13,7 +13,6 @@ dependencies: sdk: flutter flutter_riverpod: 3.2.1 logger: ^2.7.0 - sentry_flutter: ^9.26.0 dev_dependencies: flutter_lints: ^6.0.0 diff --git a/packages/core_logging/test/core_logging_test.dart b/packages/core_logging/test/core_logging_test.dart index eeef0df..9392808 100644 --- a/packages/core_logging/test/core_logging_test.dart +++ b/packages/core_logging/test/core_logging_test.dart @@ -53,85 +53,19 @@ void main() { }); }); - group('LogBuffer', () { - test('超出容量丢最旧的', () { - final LogBuffer buffer = LogBuffer(capacity: 3); - for (int i = 0; i < 5; i++) { - buffer.add(LogRecord(level: 'I', message: '$i', timestamp: DateTime(2026))); - } - expect(buffer.length, 3); - expect(buffer.recent().map((LogRecord r) => r.message), ['2', '3', '4']); - }); - - test('recent 默认只取 30 条', () { - final LogBuffer buffer = LogBuffer(); - for (int i = 0; i < 500; i++) { - buffer.add(LogRecord(level: 'I', message: '$i', timestamp: DateTime(2026))); - } - expect(buffer.length, 500); - expect(buffer.recent().length, 30); - expect(buffer.recent().first.message, '470'); - }); - }); - group('LoggerAppLogger', () { AppEnv envOf(AppFlavor flavor) => AppEnv( flavor: flavor, apiBaseUrl: 'https://example.com', enableLog: true, - sentryDsn: '', h5AllowedHosts: const {}, ); - test('prod 只收 warning 及以上', () { - final LogBuffer buffer = LogBuffer(); - final AppLogger log = LoggerAppLogger(env: envOf(AppFlavor.prod), buffer: buffer); - - log - ..d('debug') - ..i('info') - ..w('warn') - ..e('error'); - - expect(buffer.recent().map((LogRecord r) => r.message), ['warn', 'error']); - }); - - test('dev 收全部级别', () { - final LogBuffer buffer = LogBuffer(); - LoggerAppLogger(env: envOf(AppFlavor.dev), buffer: buffer) - ..d('debug') - ..i('info'); - - expect(buffer.length, 2); - }); - - test('结构化 data 进缓冲前已脱敏', () { - final LogBuffer buffer = LogBuffer(); - LoggerAppLogger( - env: envOf(AppFlavor.uat), - buffer: buffer, - ).i('login', data: {'phone': '13812345678'}); - - expect(buffer.recent().single.message, contains('138****5678')); - expect(buffer.recent().single.message, isNot(contains('13812345678'))); - }); - }); - - group('throwTestException', () { - AppEnv envOf(AppFlavor flavor) => AppEnv( - flavor: flavor, - apiBaseUrl: 'https://example.com', - enableLog: false, - sentryDsn: '', - h5AllowedHosts: const {}, - ); - - test('dev 抛测试异常', () { - expect(() => throwTestException(envOf(AppFlavor.dev)), throwsA(isA())); - }); - - test('prod 拒绝调用', () { - expect(() => throwTestException(envOf(AppFlavor.prod)), throwsA(isA())); + test('各环境均可创建日志器', () { + for (final AppFlavor flavor in AppFlavor.values) { + final AppLogger log = LoggerAppLogger(env: envOf(flavor)); + log.i('login', data: {'phone': '13812345678'}); + } }); }); } diff --git a/pubspec.lock b/pubspec.lock index 60029c2..f478771 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,14 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.2+1" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.dev" - source: hosted - version: "2.0.3" args: dependency: transitive description: @@ -407,14 +399,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" - globbing: - dependency: transitive - description: - name: globbing - sha256: "4f89cfaf6fa74c9c1740a96259da06bd45411ede56744e28017cc534a12b6e2d" - url: "https://pub.dev" - source: hosted - version: "1.0.0" go_router: dependency: transitive description: @@ -463,14 +447,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - injector: - dependency: transitive - description: - name: injector - sha256: ed389bed5b48a699d5b9561c985023d0d5cc88dd5ff2237aadcce5a5ab433e4e - url: "https://pub.dev" - source: hosted - version: "3.0.0" integration_test: dependency: transitive description: flutter @@ -492,14 +468,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - jni: - dependency: transitive - description: - name: jni - sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 - url: "https://pub.dev" - source: hosted - version: "0.14.2" json_annotation: dependency: transitive description: @@ -644,22 +612,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - package_info_plus: - dependency: transitive - description: - name: package_info_plus - sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" - url: "https://pub.dev" - source: hosted - version: "10.2.1" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 - url: "https://pub.dev" - source: hosted - version: "4.1.0" path: dependency: transitive description: @@ -772,14 +724,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - properties: - dependency: transitive - description: - name: properties - sha256: "333f427dd4ed07bdbe8c75b9ff864a1e70b5d7a8426a2e8bdd457b65ae5ac598" - url: "https://pub.dev" - source: hosted - version: "2.1.1" pub_semver: dependency: transitive description: @@ -844,30 +788,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.3" - sentry: - dependency: transitive - description: - name: sentry - sha256: c2ecd8abe82e63cdcb6947f71320612ced56e10ba94db7529d85cc02be47cb3b - url: "https://pub.dev" - source: hosted - version: "9.27.0" - sentry_dart_plugin: - dependency: transitive - description: - name: sentry_dart_plugin - sha256: da9c1d0b3c87a251bfc36301f16af090a88c2d59128fe9a6f908f5ac20340c97 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - sentry_flutter: - dependency: transitive - description: - name: sentry_flutter - sha256: ec89cc6ba939ca19155ea83900d9740a36544f50b3b6baf265518e3348fb0f50 - url: "https://pub.dev" - source: hosted - version: "9.27.0" shared_preferences: dependency: transitive description: @@ -1041,14 +961,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" - system_info2: - dependency: transitive - description: - name: system_info2 - sha256: b937736ecfa63c45b10dde1ceb6bb30e5c0c340e14c441df024150679d65ac43 - url: "https://pub.dev" - source: hosted - version: "4.1.0" term_glyph: dependency: transitive description: -- 2.54.0 From 3a7f7445606ff7125697b42ee3b4fdd7ab55693e Mon Sep 17 00:00:00 2001 From: "Guangfei.Zhao" Date: Thu, 3 Sep 2026 13:54:28 +0800 Subject: [PATCH 4/5] refactor: update app name to Conti-Retail-App across multiple files --- README.md | 2 +- app/android/app/build.gradle.kts | 8 ++++---- app/ios/Flutter/Dev.xcconfig | 2 +- app/ios/Flutter/Prod.xcconfig | 2 +- app/ios/Flutter/Uat.xcconfig | 2 +- app/lib/l10n/app_localizations.dart | 2 +- app/lib/l10n/app_localizations_zh.dart | 2 +- app/lib/l10n/app_zh.arb | 2 +- app/lib/src/app_widget.dart | 2 +- pubspec.lock | 16 ++++++++-------- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2d5ee49..2472d04 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # conti-retail-app -大陆马门店 App。Flutter + Melos 单仓多包。 +Conti-Retail-App。Flutter + Melos 单仓多包。 架构约束全部来自相邻仓库 `conti-docs/` 的 01–14 号文档。**本仓库对这些文档的 偏离,逐条记在 [SCAFFOLD-NOTES.md](SCAFFOLD-NOTES.md)**——那份文件是回写文档的 diff --git a/app/android/app/build.gradle.kts b/app/android/app/build.gradle.kts index bf203ee..4a82f76 100644 --- a/app/android/app/build.gradle.kts +++ b/app/android/app/build.gradle.kts @@ -36,7 +36,7 @@ android { // 1. 用 applicationIdSuffix 而不是覆盖 applicationId——后者一改,Firebase / // 推送 / 应用市场的包名对应关系全部要重配一遍; // 2. app_name 走 resValue,让三个环境在桌面上一眼能分清。装了三个图标都叫 - // "大陆马门店"的时候,测试提的 bug 会有一半定位不到环境; + // "Conti-Retail-App" 的时候,测试提的 bug 会有一半定位不到环境; // 3. prod 没有后缀,就是正式包名。 // ------------------------------------------------------------------------ flavorDimensions += "env" @@ -44,16 +44,16 @@ android { create("dev") { dimension = "env" applicationIdSuffix = ".dev" - resValue("string", "app_name", "马店(开发)") + resValue("string", "app_name", "Conti-Retail-App (Dev)") } create("uat") { dimension = "env" applicationIdSuffix = ".uat" - resValue("string", "app_name", "马店(测试)") + resValue("string", "app_name", "Conti-Retail-App (UAT)") } create("prod") { dimension = "env" - resValue("string", "app_name", "大陆马门店") + resValue("string", "app_name", "Conti-Retail-App") } } diff --git a/app/ios/Flutter/Dev.xcconfig b/app/ios/Flutter/Dev.xcconfig index 1db6ec6..892ca82 100644 --- a/app/ios/Flutter/Dev.xcconfig +++ b/app/ios/Flutter/Dev.xcconfig @@ -4,7 +4,7 @@ // 这里只放**环境差异**,公共配置留在 Generated.xcconfig / Debug.xcconfig。 BUNDLE_ID_SUFFIX=.dev -APP_DISPLAY_NAME=马店(开发) +APP_DISPLAY_NAME=Conti-Retail-App (Dev) // 包名不覆盖、只加后缀——和 Android 的 applicationIdSuffix 保持同一套规则。 PRODUCT_BUNDLE_IDENTIFIER=com.conti.retail$(BUNDLE_ID_SUFFIX) diff --git a/app/ios/Flutter/Prod.xcconfig b/app/ios/Flutter/Prod.xcconfig index 46c592b..573de81 100644 --- a/app/ios/Flutter/Prod.xcconfig +++ b/app/ios/Flutter/Prod.xcconfig @@ -4,7 +4,7 @@ // 这里只放**环境差异**,公共配置留在 Generated.xcconfig / Debug.xcconfig。 BUNDLE_ID_SUFFIX= -APP_DISPLAY_NAME=大陆马门店 +APP_DISPLAY_NAME=Conti-Retail-App // 包名不覆盖、只加后缀——和 Android 的 applicationIdSuffix 保持同一套规则。 PRODUCT_BUNDLE_IDENTIFIER=com.conti.retail$(BUNDLE_ID_SUFFIX) diff --git a/app/ios/Flutter/Uat.xcconfig b/app/ios/Flutter/Uat.xcconfig index b60f830..1608357 100644 --- a/app/ios/Flutter/Uat.xcconfig +++ b/app/ios/Flutter/Uat.xcconfig @@ -4,7 +4,7 @@ // 这里只放**环境差异**,公共配置留在 Generated.xcconfig / Debug.xcconfig。 BUNDLE_ID_SUFFIX=.uat -APP_DISPLAY_NAME=马店(测试) +APP_DISPLAY_NAME=Conti-Retail-App (UAT) // 包名不覆盖、只加后缀——和 Android 的 applicationIdSuffix 保持同一套规则。 PRODUCT_BUNDLE_IDENTIFIER=com.conti.retail$(BUNDLE_ID_SUFFIX) diff --git a/app/lib/l10n/app_localizations.dart b/app/lib/l10n/app_localizations.dart index 5a7cf69..c2d0ead 100644 --- a/app/lib/l10n/app_localizations.dart +++ b/app/lib/l10n/app_localizations.dart @@ -95,7 +95,7 @@ abstract class AppLocalizations { /// App 名称。目前只有这一条——其余文案等 16-i18n.md 定了方案再统一迁入。 /// /// In zh, this message translates to: - /// **'大陆马门店'** + /// **'Conti-Retail-App'** String get appTitle; } diff --git a/app/lib/l10n/app_localizations_zh.dart b/app/lib/l10n/app_localizations_zh.dart index 348eced..3bc489d 100644 --- a/app/lib/l10n/app_localizations_zh.dart +++ b/app/lib/l10n/app_localizations_zh.dart @@ -9,5 +9,5 @@ class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); @override - String get appTitle => '大陆马门店'; + String get appTitle => 'Conti-Retail-App'; } diff --git a/app/lib/l10n/app_zh.arb b/app/lib/l10n/app_zh.arb index 8040dff..b4ba006 100644 --- a/app/lib/l10n/app_zh.arb +++ b/app/lib/l10n/app_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "appTitle": "大陆马门店", + "appTitle": "Conti-Retail-App", "@appTitle": { "description": "App 名称。目前只有这一条——其余文案等 16-i18n.md 定了方案再统一迁入。" } diff --git a/app/lib/src/app_widget.dart b/app/lib/src/app_widget.dart index 5452457..3e478a3 100644 --- a/app/lib/src/app_widget.dart +++ b/app/lib/src/app_widget.dart @@ -39,7 +39,7 @@ class ContiApp extends ConsumerWidget { ); return MaterialApp.router( - title: '大陆马门店', + title: 'Conti-Retail-App', theme: AppTheme.light, darkTheme: AppTheme.dark, routerConfig: router, diff --git a/pubspec.lock b/pubspec.lock index f478771..af0f87a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -552,10 +552,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -973,26 +973,26 @@ packages: dependency: transitive description: name: test - sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.31.0" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.17" + version: "0.6.16" typed_data: dependency: transitive description: -- 2.54.0 From 72f6a30aff7165d9131cf2e5d39ec92c70b6f9e5 Mon Sep 17 00:00:00 2001 From: "Guangfei.Zhao" Date: Thu, 3 Sep 2026 16:39:53 +0800 Subject: [PATCH 5/5] feat: enhance device ID generation with timeout and refactor shared_preferences dependency for OHOS compatibility --- app/lib/src/device_id.dart | 27 ++- app/ohos/build-profile.json5 | 17 +- docs/15-flutter-sdk-cli.md | 273 +++++++++++++++++++++++++++++ docs/README.md | 1 + packages/core_storage/pubspec.yaml | 10 +- pubspec.lock | 20 ++- 6 files changed, 336 insertions(+), 12 deletions(-) create mode 100644 docs/15-flutter-sdk-cli.md diff --git a/app/lib/src/device_id.dart b/app/lib/src/device_id.dart index 3a3c479..9be692a 100644 --- a/app/lib/src/device_id.dart +++ b/app/lib/src/device_id.dart @@ -15,15 +15,34 @@ import 'package:core_storage/core_storage.dart'; /// 这里的语义是"这次安装":卸载重装换一个新 ID 是**预期行为**,不需要跨安装 /// 追踪——它的用途只有一个,把同一台设备的日志串起来排查问题。 /// --------------------------------------------------------------------------- -Future loadOrCreateDeviceId(Prefs prefs) async { +Future loadOrCreateDeviceId( + Prefs prefs, { + Duration timeout = const Duration(seconds: 2), +}) async { const String key = 'device_id'; + + try { + return await _loadOrCreatePersistedDeviceId(prefs, key).timeout(timeout); + } on Object { + // 本地存储只增强日志关联,不能因未注册的平台插件阻断 runApp()。 + // 当前值仅在本次进程有效;下次启动仍会尝试读写持久化 ID。 + return _newDeviceId(); + } +} + +Future _loadOrCreatePersistedDeviceId(Prefs prefs, String key) async { final String? existing = await prefs.getString(key); if (existing != null && existing.isNotEmpty) { return existing; } - final Random random = Random.secure(); - final List bytes = List.generate(16, (int _) => random.nextInt(256)); - final String created = base64Url.encode(bytes).replaceAll('=', ''); + + final String created = _newDeviceId(); await prefs.setString(key, created); return created; } + +String _newDeviceId() { + final Random random = Random.secure(); + final List bytes = List.generate(16, (int _) => random.nextInt(256)); + return base64Url.encode(bytes).replaceAll('=', ''); +} diff --git a/app/ohos/build-profile.json5 b/app/ohos/build-profile.json5 index 1c112b1..034b3ce 100644 --- a/app/ohos/build-profile.json5 +++ b/app/ohos/build-profile.json5 @@ -1,7 +1,20 @@ - { "app": { - "signingConfigs": [], + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\A200477427\\.ohos\\config\\default_ohos_yW8CJeJiNlUT6oywq41Uc115VHPdXxvAbtRnmkeaxSQ=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B3CF04C0E3E6DBB38D72D64B7C8B65A62F75FFA5603AC50AB5C3364CB0AA1E2A480FC6D25DF6CFF", + "profile": "C:\\Users\\A200477427\\.ohos\\config\\default_ohos_yW8CJeJiNlUT6oywq41Uc115VHPdXxvAbtRnmkeaxSQ=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\A200477427\\.ohos\\config\\default_ohos_yW8CJeJiNlUT6oywq41Uc115VHPdXxvAbtRnmkeaxSQ=.p12", + "storePassword": "0000001BE9AAD820D3FF8C4809435B90558226EFFB2EF6F5A57909A601E5B27596F032906E5088119F3BC0" + } + } + ], "products": [ { "name": "default", diff --git a/docs/15-flutter-sdk-cli.md b/docs/15-flutter-sdk-cli.md new file mode 100644 index 0000000..4193654 --- /dev/null +++ b/docs/15-flutter-sdk-cli.md @@ -0,0 +1,273 @@ +# 15. Flutter SDK CLI:官方 SDK 与 OpenHarmony SDK 速查 + +本文是本仓库的 **Flutter 命令行总手册**:先记录官方 Flutter SDK 的通用开发命令,再标记 CPF-Flutter OpenHarmony(OHOS)SDK 所增加的命令、环境和限制。它不是只讲 OHOS 的手册。 + +同一套 Dart / Flutter 业务代码通常使用相同的 CLI;区别主要在可用目标平台、原生构建工具、构建产物及插件实现。开发前先选定本次终端使用的 SDK,避免官方 Flutter 与 CPF-Flutter 混用。 + +> 本文是开发操作指南,不替代产品上线验收。首版正式交付范围仍以 [README](./README.md) 所述 Android / iOS 为准;OHOS 的真机、插件和原生能力须分别验收。 + +## 1. 先选 SDK:官方 Flutter 还是 OHOS Flutter + +| 目标 | 应使用的 SDK | 典型命令 | +| --- | --- | --- | +| Android、iOS、Web、Windows、macOS、Linux | 官方 Flutter SDK | `flutter run`、`flutter build apk`、`flutter build appbundle`、`flutter build ipa`、`flutter build web` 等 | +| OpenHarmony | CPF-Flutter OHOS SDK | `flutter run -d `、`flutter build hap` | +| 同时维护 Android / iOS / OHOS | CPF-Flutter OHOS SDK(项目和插件须完成 OHOS 兼容验证) | `flutter run -d <对应设备>`、按目标使用 `build apk` / `build hap` | + +检查当前终端实际使用哪个 SDK: + +```powershell +Get-Command flutter -All +flutter --version +``` + +不要把两个 SDK 的 `bin` 目录同时放在 `PATH` 前部并期待 Flutter 自动切换。`flutter` 命令只会使用 PATH 中排在最前的那个 SDK。需要明确切换时,调整 PATH、使用 FVM,或直接用目标 SDK 的 `flutter.bat` 完整路径执行。 + +### 官方 Flutter SDK 的通用命令 + +以下命令来自 Flutter CLI 本身,官方 Flutter SDK 与 CPF-Flutter OHOS SDK 都会保留(具体平台能力由当前 SDK 与目标设备决定): + +| 场景 | 命令 | 说明 | +| --- | --- | --- | +| 查看版本 | `flutter --version` | 确认实际 SDK 与 Dart 版本 | +| 检查环境 | `flutter doctor -v` | 检查平台工具链与已连接设备 | +| 配置 SDK | `flutter config -- ` | 查看:`flutter config --list` | +| 新建 App | `flutter create --org ` | 默认创建当前 SDK 支持的平台 | +| 列出设备 | `flutter devices` | 获取 `deviceId` | +| 启动调试 | `flutter run -d ` | 构建、安装、运行、连接调试器 | +| 连接已运行应用 | `flutter attach -d ` | 不重新安装应用 | +| 截屏 | `flutter screenshot -d -o .\screen.png` | 默认截取设备完整屏幕 | +| 静态分析 | `flutter analyze` | 按 `analysis_options.yaml` 分析代码 | +| 执行测试 | `flutter test` | 执行 unit / widget 测试 | +| 获取依赖 | `flutter pub get` | 按 `pubspec.yaml` 解析依赖 | +| 更新依赖 | `flutter pub upgrade` | 更新允许范围内的依赖版本 | +| 查看依赖状态 | `flutter pub outdated` | 查看可升级依赖 | +| 清理项目 | `flutter clean` | 清理当前工程的构建与工具缓存 | +| 修复全局缓存 | `flutter pub cache repair` | 重新下载全局 Pub 缓存中的包 | +| 清空全局缓存 | `flutter pub cache clean` | 影响所有 Flutter / Dart 项目,最后手段 | + +### 官方 Flutter SDK 的常见构建目标 + +这些是官方 SDK 的常用构建命令;是否可用取决于该平台的工具链与项目是否已生成对应目录。 + +```powershell +flutter build apk --debug # Android 测试 APK +flutter build apk --release # Android 正式 APK +flutter build appbundle --release # Android Play 上架 AAB +flutter build ipa --release # iOS IPA(仅 macOS + Xcode 环境) +flutter build web --release # Web 静态产物 +flutter build windows --release # Windows 桌面应用(Windows + Visual Studio) +flutter build macos --release # macOS 桌面应用(仅 macOS) +flutter build linux --release # Linux 桌面应用(仅 Linux) +``` + +`flutter assemble` 是底层 Flutter 资源构建入口,通常由上述 `flutter build ` 自动调用;日常不应把它当作完整应用打包命令。 + +## 2. 当前项目状态 + +`app/` 已经是一个包含 OpenHarmony 平台目录的 Flutter 项目,**不要在 `app/` 目录重新执行 `flutter create`**。这会以模板补全/修复工程,可能改写已有原生配置。 + +| 项目 | 当前值 / 状态 | +| --- | --- | +| Flutter SDK | CPF-Flutter `3.41.10-ohos-1.0.0` | +| Flutter SDK 路径 | `C:\Users\A200477427\Developments\flutter_flutter` | +| OHOS 工程 | `app/ohos/` 已存在 | +| DevEco OHOS SDK | `C:\Program Files\Huawei\DevEco Studio\sdk` | +| 已验证模拟器 | `127.0.0.1:5555`,`ohos-x64` | +| Flutter OHOS 开关 | 已启用:`enable-ohos` | + +## 3. OpenHarmony 专用:一次性环境配置(Windows) + +CPF-Flutter 不是官方 Flutter release channel,因此 `flutter doctor` 可能提示非标准 remote / channel。这是该 OHOS SDK fork 的预期提示;不要为消除这条提示切回标准 Flutter SDK。 + +### Flutter 配置 + +```powershell +flutter config --enable-ohos +flutter config --ohos-sdk "C:\Program Files\Huawei\DevEco Studio\sdk" +flutter config --ohpm-home "C:\Program Files\Huawei\DevEco Studio\tools\ohpm" +``` + +### Windows 用户级 PATH + +将下面两个目录加到**用户环境变量** `Path`,然后重启 PowerShell、VS Code / Android Studio: + +```text +C:\Program Files\Huawei\DevEco Studio\tools\ohpm\bin +C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin +``` + +可选地设置: + +```text +OHPM_HOME=C:\Program Files\Huawei\DevEco Studio\tools\ohpm +``` + +仅对当前 PowerShell 临时生效时: + +```powershell +$env:Path = "C:\Program Files\Huawei\DevEco Studio\tools\ohpm\bin;C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin;$env:Path" +``` + +### 验证 + +```powershell +flutter doctor -v +flutter devices +``` + +必须看到: + +```text +[✓] HarmonyOS toolchain - develop for HarmonyOS devices +``` + +若未看到,优先检查 `ohpm` 和 `hvigorw` 是否能在新打开的 PowerShell 中直接执行,以及 DevEco Studio 是否安装了 OHOS SDK。 + +## 4. OpenHarmony 专用:日常运行、构建与安装 + +所有项目命令从 `app/`(包含 `pubspec.yaml` 的目录)运行: + +```powershell +cd C:\Users\A200477427\Developers\Continental-App\conti-retail-app\app +``` + +### 模拟器运行 + +```powershell +flutter pub get +flutter devices +flutter run -d 127.0.0.1:5555 +``` + +`flutter run` 会构建、安装、启动应用,并连接调试器。运行期间:`r` 热重载、`R` 热重启、`q` 退出调试。 + +### HAP 构建 + +```powershell +# x64 模拟器测试包 +flutter build hap --debug --target-platform ohos-x64 + +# ARM64 真机测试包(默认目标也为 ohos-arm64) +flutter build hap --debug --target-platform ohos-arm64 + +# ARM64 真机正式包 +flutter build hap --release --target-platform ohos-arm64 +``` + +日常使用 SDK 自带的 OHOS Engine,不需 `--local-engine`。仅在自行编译了兼容 OHOS 的 Flutter Engine 时才传入该路径: + +```powershell +flutter build hap --debug --target-platform ohos-arm64 --local-engine="C:\path\to\ohos_debug_engine" +``` + +### 安装、附加调试和截图 + +```powershell +# 构建并安装到指定设备(本机 SDK 的实际参数为 -d) +flutter install -d 127.0.0.1:5555 --debug + +# 附加到已运行应用,不重新构建/安装 +flutter attach -d 127.0.0.1:5555 + +# 截取完整设备屏幕 +flutter screenshot -d 127.0.0.1:5555 -o .\ohos-screenshot.png +``` + +部分历史 OHOS SDK 文档将已有 HAP 的安装写为 `flutter install -t `。应以当前 SDK 的 `flutter help install` 为准;本项目当前 SDK 显示的是 `-d` / `--device-id`,且不将 HAP 路径作为该命令的位置参数。 + +## 5. OpenHarmony 扩展命令速查 + +`[]` 和 `<>` 均是文档占位符,实际执行时不要输入。 + +| 指令 | 作用 | 常用示例 | +| --- | --- | --- | +| `doctor` | 检查开发环境 | `flutter doctor -v` | +| `config` | 配置 OHOS SDK 与工具 | `flutter config --enable-ohos` | +| `create`(App) | 创建独立多端应用 | `flutter create --platforms ohos,android,ios --org com.conti my_app` | +| `create`(Module) | 创建嵌入原生应用的 Flutter module | `flutter create -t module my_module` | +| `create`(Plugin) | 创建 MethodChannel 插件 | `flutter create -t plugin --platforms ohos,android,ios my_plugin` | +| `create`(Plugin FFI) | 创建基于 Dart FFI 的插件 | `flutter create -t plugin_ffi --platforms ohos,android,ios my_plugin_ffi` | +| `devices` | 查找已连接设备 | `flutter devices` | +| `assemble` | 构建 Flutter 底层资源 | `flutter assemble`(通常由 `build hap` 自动调用) | +| `build hap` | 构建 OHOS HAP 安装包 | `flutter build hap --debug` / `--release` | +| `run` | 构建、安装、启动并调试 | `flutter run -d ` | +| `install` | 构建并安装到设备 | `flutter install -d --debug` | +| `attach` | 附加到已运行应用 | `flutter attach -d ` | +| `screenshot` | 截取设备屏幕 | `flutter screenshot -d -o .\screen.png` | +| `pub get` | 获取当前项目依赖 | `flutter pub get` | +| `clean` | 清理当前项目构建缓存 | `flutter clean` | +| `pub cache` | 管理全局 Pub 缓存 | `flutter pub cache repair` | +| `pub cache clean` | 清空全局 Pub 缓存 | `flutter pub cache clean` | + +### 依赖与缓存的正确清理顺序 + +```powershell +# 首选:只影响当前项目 +flutter clean +flutter pub get + +# 缓存确实损坏时:修复整机 Pub 缓存 +flutter pub cache repair + +# 最后手段:清空整机所有 Pub 缓存,再重新下载 +flutter pub cache clean +flutter pub get +``` + +`flutter pub cache clean` 会影响电脑上的所有 Flutter / Dart 项目,不能作为普通构建失败时的第一反应。 + +## 6. CPF-Flutter OHOS SDK 与标准 Flutter SDK 的区别 + +| 维度 | 标准 Flutter SDK | CPF-Flutter OHOS SDK | +| --- | --- | --- | +| 维护与发行 | Flutter 官方渠道及其 Engine | CPF-Flutter 维护的 OpenHarmony 适配发行版 | +| 新增目标平台 | Android、iOS、Web、桌面等 | 在 Flutter 工具链中增加 `ohos` | +| 原生工程目录 | `android/`、`ios/` 等 | 额外生成 `ohos/` | +| 产物 | Android APK/AAB、iOS IPA 等 | OpenHarmony `.hap` | +| 构建命令 | `flutter build apk`、`appbundle`、`ipa` 等 | `flutter build hap --debug` / `--release` | +| 原生构建工具 | Gradle、Xcode 等 | DevEco Studio SDK、`ohpm`、`hvigorw` | +| 设备发现/运行 | `flutter devices` / `flutter run` | 命令保持相同,但发现并运行 OHOS 实机/模拟器 | +| 插件实现 | 各平台的官方 embedding / API | 插件必须有 OHOS 适配实现;并非所有 pub.dev 插件天然可用 | +| SDK 版本标识 | 例如 `3.41.x stable` | 例如 `3.41.10-ohos-1.0.0`,带 OHOS fork 后缀 | + +### 不变的部分 + +- Dart、Flutter Widget、`pubspec.yaml`、`flutter pub get`、Widget / 单元测试的开发体验基本相同。 +- `flutter devices`、`flutter run`、`flutter attach`、`flutter screenshot` 仍是主要 CLI 入口。 +- 应用业务层不应因为增加 OHOS 平台而把平台判断散落到 feature 中;平台差异应集中在插件或原生集成层。 + +### 需要额外注意的部分 + +1. **插件兼容性不是自动获得的。** 每个含原生能力的依赖(相机、扫码、WebView、推送、存储、权限等)都必须确认其是否有 OHOS implementation,并在 OHOS 模拟器与真机分别验证。 +2. **OHOS 使用 DevEco 构建链。** `flutter build hap` 内部会准备 Flutter 资源,再由 `ohpm` / `hvigorw` 完成 HAP 组装;前述工具未在 PATH 时 `flutter doctor` 会失败。 +3. **模拟器与真机架构不同。** 当前模拟器是 `ohos-x64`;大多数真机是 `ohos-arm64`。构建/测试时应明确目标。 +4. **本地 Engine 是高级路径。** `--local-engine` 仅用于 SDK / Engine 开发或特定兼容性验证,普通业务开发使用随 CPF-Flutter SDK 提供的 Engine。 +5. **Windows 路径约束。** CPF-Flutter 文档提示,在 Windows 上 Flutter 项目与依赖插件项目应位于同一磁盘分区,避免宿主构建/插件解析出现路径问题。 + +## 7. 故障排查顺序 + +```text +flutter doctor -v + ↓ +确认 HarmonyOS toolchain 为 ✓ + ↓ +flutter devices + ↓ +确认目标设备 ID 与架构 + ↓ +flutter clean && flutter pub get + ↓ +flutter run -d +``` + +- `ohpm` / `hvigorw` 缺失:确认第 3 节 PATH 配置,重开终端后再执行 `flutter doctor -v`。 +- 设备未出现:确认模拟器已启动或真机已开启开发者模式/USB 调试,然后再次 `flutter devices`。 +- 构建残留导致异常:先 `flutter clean`,再 `flutter pub get`;不要直接清除全局缓存。 +- 某个原生功能不可用:先检查对应插件 OHOS 兼容性与 `ohos/` 侧实现,不要默认 Dart 层代码跨平台即代表原生能力可用。 + +## 8. 来源与版本原则 + +- [CPF-Flutter / flutter_flutter](https://gitcode.com/CPF-Flutter/flutter_flutter):OHOS Flutter SDK、支持命令与版本分支。 +- [OpenHarmony-SIG flutter_flutter README](https://gitee.com/openharmony-sig/flutter_flutter/blob/master/README.en.md):环境要求、命令与 OpenHarmony 适配说明。 +- 本机行为以 `flutter --version`、`flutter help ` 和 `flutter doctor -v` 为准。OHOS fork 版本演进很快,历史文档中的命令参数若与当前 CLI 帮助不一致,应以当前 CLI 为准。 diff --git a/docs/README.md b/docs/README.md index c1e7cbb..9f10013 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,7 @@ Flutter APP 的分包、分层、技术选型等决策记录,按序号阅读 | [12-error-and-api-contract.md](./12-error-and-api-contract.md) | 错误处理与 API 契约(`ApiResult`、异常体系、降级) | | [13-observability-analytics.md](./13-observability-analytics.md) | 可观测性与埋点(Sentry 崩溃上报、神策客户端埋点、日志脱敏) | | [14-conventions-and-ci-gates.md](./14-conventions-and-ci-gates.md) | 工程规范与 CI 门禁(lint、格式化、分支、流水线卡点) | +| [15-flutter-sdk-cli.md](./15-flutter-sdk-cli.md) | Flutter SDK CLI 速查(官方通用命令、OHOS 环境/构建与 SDK 差异) | ## 这份副本从哪来、怎么更新 diff --git a/packages/core_storage/pubspec.yaml b/packages/core_storage/pubspec.yaml index 11a1350..45be5a7 100644 --- a/packages/core_storage/pubspec.yaml +++ b/packages/core_storage/pubspec.yaml @@ -19,7 +19,15 @@ dependencies: flutter: sdk: flutter flutter_riverpod: 3.2.1 - shared_preferences: ^2.5.5 + # Pub.dev 的 shared_preferences 不声明 OHOS 实现,Flutter 因而不会把它 + # 注册进 GeneratedPluginRegistrant.ets。固定到 CPF-Flutter 的 OHOS 适配 + # 分支;该主包会解析 shared_preferences_ohos。 + shared_preferences: + git: + url: https://gitcode.com/CPF-Flutter/flutter_packages.git + # 固定 commit,避免 Pub 在 Windows Git cache 中错误复用 master 工作树。 + ref: 19bd50ff6d5eaa96f18c63796c51e1b4e78a7480 + path: packages/shared_preferences/shared_preferences dev_dependencies: flutter_lints: ^6.0.0 diff --git a/pubspec.lock b/pubspec.lock index af0f87a..210f1e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -791,11 +791,12 @@ packages: shared_preferences: dependency: transitive description: - name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.dev" - source: hosted - version: "2.5.5" + path: "packages/shared_preferences/shared_preferences" + ref: "19bd50ff6d5eaa96f18c63796c51e1b4e78a7480" + resolved-ref: "19bd50ff6d5eaa96f18c63796c51e1b4e78a7480" + url: "https://gitcode.com/CPF-Flutter/flutter_packages.git" + source: git + version: "2.5.4" shared_preferences_android: dependency: transitive description: @@ -820,6 +821,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shared_preferences_ohos: + dependency: transitive + description: + path: "packages/shared_preferences/shared_preferences_ohos" + ref: "br_shared_preferences-v2.5.4_ohos" + resolved-ref: "19bd50ff6d5eaa96f18c63796c51e1b4e78a7480" + url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" + source: git + version: "2.5.4" shared_preferences_platform_interface: dependency: transitive description: -- 2.54.0