From 31429e12e0f57c8cc11347a060c2a502e11445a7 Mon Sep 17 00:00:00 2001 From: zzhyyyyy <2685922758@qq.com> Date: Fri, 29 May 2026 13:27:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96:=E5=A4=9A=E6=AF=94?= =?UTF-8?q?=E6=AF=94Android=E5=AE=A2=E6=88=B7=E7=AB=AF(=E5=8D=A0=E5=9D=91?= =?UTF-8?q?=E7=89=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- duobibi(mock)Android/duobibi-mock/.gitignore | 17 + duobibi(mock)Android/duobibi-mock/README.md | 166 +++++ .../duobibi-mock/app/build.gradle.kts | 139 ++++ .../libs/MiPush_SDK_Client_7_9_2-C_3rd.aar | Bin 0 -> 1008221 bytes .../duobibi-mock/app/proguard-rules.pro | 59 ++ .../app/src/main/AndroidManifest.xml | 115 +++ .../jishisongfu/duobibi/DuobibiApplication.kt | 46 ++ .../com/jishisongfu/duobibi/MainActivity.kt | 110 +++ .../com/jishisongfu/duobibi/auth/JgInit.kt | 108 +++ .../jishisongfu/duobibi/auth/OneClickLogin.kt | 70 ++ .../duobibi/auth/PhoneBindViewModel.kt | 88 +++ .../jishisongfu/duobibi/data/api/ArenaApi.kt | 37 + .../jishisongfu/duobibi/data/api/AuthApi.kt | 27 + .../jishisongfu/duobibi/data/api/OcrApi.kt | 53 ++ .../jishisongfu/duobibi/data/api/ParseApi.kt | 13 + .../jishisongfu/duobibi/data/api/ParseDto.kt | 40 ++ .../duobibi/data/api/ProtectApi.kt | 46 ++ .../duobibi/data/api/WorthBuyApi.kt | 34 + .../duobibi/data/db/AppDatabase.kt | 19 + .../jishisongfu/duobibi/data/db/Cluster.kt | 19 + .../jishisongfu/duobibi/data/db/ClusterDao.kt | 39 + .../jishisongfu/duobibi/data/db/PriceAlert.kt | 50 ++ .../duobibi/data/db/PriceAlertDao.kt | 61 ++ .../duobibi/data/db/PriceRecord.kt | 16 + .../duobibi/data/db/PriceRecordDao.kt | 50 ++ .../duobibi/data/repo/PriceRepository.kt | 461 ++++++++++++ .../jishisongfu/duobibi/di/DatabaseModule.kt | 34 + .../jishisongfu/duobibi/di/NetworkModule.kt | 125 ++++ .../floating/AccessibilityServiceHolder.kt | 34 + .../duobibi/floating/FloatingButtonView.kt | 103 +++ .../jishisongfu/duobibi/floating/NodeDto.kt | 12 + .../duobibi/floating/TreeSerializer.kt | 53 ++ .../duobibi/heartbeat/A11yStateProbe.kt | 30 + .../duobibi/heartbeat/AppForegroundProbe.kt | 37 + .../duobibi/heartbeat/DeviceIdProvider.kt | 23 + .../duobibi/heartbeat/HeartbeatPayload.kt | 31 + .../duobibi/heartbeat/HeartbeatReceiver.kt | 42 ++ .../duobibi/heartbeat/HeartbeatScheduler.kt | 57 ++ .../duobibi/heartbeat/HeartbeatSender.kt | 99 +++ .../duobibi/push/MiPushBootstrap.kt | 34 + .../duobibi/push/MiPushReceiver.kt | 86 +++ .../duobibi/service/FloatingWindowService.kt | 247 +++++++ .../service/PriceBotAccessibilityService.kt | 118 +++ .../com/jishisongfu/duobibi/ui/DbbWidgets.kt | 244 +++++++ .../java/com/jishisongfu/duobibi/ui/Haptic.kt | 40 ++ .../com/jishisongfu/duobibi/ui/HeroBanner.kt | 109 +++ .../jishisongfu/duobibi/ui/ProductImageMap.kt | 102 +++ .../com/jishisongfu/duobibi/ui/UiCommon.kt | 104 +++ .../duobibi/ui/alert/AlertScreen.kt | 552 +++++++++++++++ .../duobibi/ui/alert/AlertViewModel.kt | 76 ++ .../duobibi/ui/arena/ArenaCategory.kt | 92 +++ .../duobibi/ui/arena/ArenaScreen.kt | 627 ++++++++++++++++ .../duobibi/ui/arena/ArenaViewModel.kt | 87 +++ .../duobibi/ui/detail/ProductDetailScreen.kt | 626 ++++++++++++++++ .../duobibi/ui/discover/DiscoverData.kt | 344 +++++++++ .../duobibi/ui/discover/DiscoverScreen.kt | 394 +++++++++++ .../duobibi/ui/discover/DiscoverViewModel.kt | 58 ++ .../duobibi/ui/discover/PriceCompareCard.kt | 203 ++++++ .../duobibi/ui/mine/AboutScreen.kt | 82 +++ .../duobibi/ui/mine/DiagnosticsScreen.kt | 105 +++ .../duobibi/ui/mine/DiagnosticsViewModel.kt | 109 +++ .../jishisongfu/duobibi/ui/mine/MineScreen.kt | 420 +++++++++++ .../duobibi/ui/mine/MineViewModel.kt | 67 ++ .../jishisongfu/duobibi/ui/nav/AppNavHost.kt | 230 ++++++ .../duobibi/ui/onboarding/OnboardingScreen.kt | 178 +++++ .../ui/privacy/PrivacyConsentScreen.kt | 53 ++ .../duobibi/ui/privacy/PrivacyPolicyScreen.kt | 76 ++ .../duobibi/ui/records/ClusterDetailScreen.kt | 458 ++++++++++++ .../ui/records/ClusterDetailViewModel.kt | 59 ++ .../duobibi/ui/records/NewRecordScreen.kt | 100 +++ .../duobibi/ui/records/NewRecordViewModel.kt | 28 + .../duobibi/ui/records/PriceTrendChart.kt | 53 ++ .../duobibi/ui/records/RecordsScreen.kt | 364 ++++++++++ .../duobibi/ui/records/RecordsViewModel.kt | 69 ++ .../com/jishisongfu/duobibi/ui/theme/Color.kt | 63 ++ .../com/jishisongfu/duobibi/ui/theme/Theme.kt | 81 +++ .../ui/tools/BundleCalculatorScreen.kt | 267 +++++++ .../ui/tools/InstallmentCalculatorScreen.kt | 287 ++++++++ .../ui/tools/UnitPriceCalculatorScreen.kt | 144 ++++ .../ui/watched/WatchedProductsScreen.kt | 184 +++++ .../duobibi/ui/worth/PriceHistory.kt | 84 +++ .../duobibi/ui/worth/WorthBuyScreen.kt | 669 ++++++++++++++++++ .../duobibi/ui/worth/WorthBuyViewModel.kt | 70 ++ .../jishisongfu/duobibi/util/AlertNotifier.kt | 76 ++ .../com/jishisongfu/duobibi/util/AuthPrefs.kt | 63 ++ .../com/jishisongfu/duobibi/util/Constants.kt | 26 + .../jishisongfu/duobibi/util/CsvExporter.kt | 96 +++ .../jishisongfu/duobibi/util/DemoDataPrefs.kt | 27 + .../duobibi/util/DemoDataSeeder.kt | 66 ++ .../duobibi/util/FloatingServiceLauncher.kt | 23 + .../duobibi/util/ImageCompressor.kt | 115 +++ .../duobibi/util/OnboardingPrefs.kt | 20 + .../duobibi/util/PermissionUtil.kt | 39 + .../duobibi/util/PlatformSearch.kt | 66 ++ .../jishisongfu/duobibi/util/PrivacyPrefs.kt | 19 + .../jishisongfu/duobibi/util/SettingsPrefs.kt | 35 + .../duobibi/util/StatsCalculator.kt | 99 +++ .../duobibi/util/VendorAutoStart.kt | 57 ++ .../jishisongfu/duobibi/util/WatchedPrefs.kt | 62 ++ .../app/src/main/res/drawable/dialog_bg.xml | 4 + .../src/main/res/drawable/ic_cat_daily.xml | 8 + .../src/main/res/drawable/ic_cat_digital.xml | 8 + .../main/res/drawable/ic_cat_ecommerce.xml | 8 + .../src/main/res/drawable/ic_cat_fashion.xml | 8 + .../src/main/res/drawable/ic_cat_grocery.xml | 12 + .../src/main/res/drawable/ic_cat_takeaway.xml | 8 + .../main/res/drawable/ic_floating_button.xml | 6 + .../res/drawable/ic_launcher_foreground.xml | 15 + .../src/main/res/drawable/ic_prod_capsule.xml | 15 + .../src/main/res/drawable/ic_prod_console.xml | 26 + .../src/main/res/drawable/ic_prod_default.xml | 12 + .../src/main/res/drawable/ic_prod_earbuds.xml | 19 + .../main/res/drawable/ic_prod_hairdryer.xml | 20 + .../src/main/res/drawable/ic_prod_phone.xml | 19 + .../app/src/main/res/drawable/img_airpods.png | Bin 0 -> 11233 bytes .../src/main/res/drawable/img_beef_rice.jpg | Bin 0 -> 55176 bytes .../src/main/res/drawable/img_dyson_hd.png | Bin 0 -> 29103 bytes .../main/res/drawable/img_dyson_vacuum.webp | Bin 0 -> 7566 bytes .../app/src/main/res/drawable/img_eggs.jpg | Bin 0 -> 11952 bytes .../main/res/drawable/img_estee_lauder.jpg | Bin 0 -> 122288 bytes .../app/src/main/res/drawable/img_greens.webp | Bin 0 -> 26672 bytes .../src/main/res/drawable/img_haidilao.webp | Bin 0 -> 14014 bytes .../src/main/res/drawable/img_haier_cup.webp | Bin 0 -> 15390 bytes .../src/main/res/drawable/img_hamburger.png | Bin 0 -> 11466 bytes .../app/src/main/res/drawable/img_heytea.jpg | Bin 0 -> 162426 bytes .../src/main/res/drawable/img_ipad_air.jpg | Bin 0 -> 26600 bytes .../main/res/drawable/img_iphone_blue.webp | Bin 0 -> 1780 bytes .../app/src/main/res/drawable/img_jdb.jpg | Bin 0 -> 38533 bytes .../app/src/main/res/drawable/img_kfc.jpg | Bin 0 -> 306024 bytes .../src/main/res/drawable/img_lancome_eye.jpg | Bin 0 -> 221038 bytes .../src/main/res/drawable/img_lululemon.webp | Bin 0 -> 4834 bytes .../app/src/main/res/drawable/img_macbook.jpg | Bin 0 -> 11716 bytes .../app/src/main/res/drawable/img_merries.png | Bin 0 -> 335015 bytes .../main/res/drawable/img_midea_kettle.jpg | Bin 0 -> 261738 bytes .../app/src/main/res/drawable/img_moutai.jpg | Bin 0 -> 131214 bytes .../src/main/res/drawable/img_nescafe.webp | Bin 0 -> 3696 bytes .../src/main/res/drawable/img_nike_af1.webp | Bin 0 -> 4300 bytes .../main/res/drawable/img_philips_shaver.png | Bin 0 -> 195387 bytes .../res/drawable/img_philips_toothbrush.jpg | Bin 0 -> 22772 bytes .../src/main/res/drawable/img_pork_belly.jpg | Bin 0 -> 340811 bytes .../main/res/drawable/img_shiseido_red.webp | Bin 0 -> 6680 bytes .../app/src/main/res/drawable/img_skii.jpg | Bin 0 -> 4017 bytes .../src/main/res/drawable/img_squirrel.jpg | Bin 0 -> 32192 bytes .../src/main/res/drawable/img_supor_pan.webp | Bin 0 -> 113530 bytes .../app/src/main/res/drawable/img_telunsu.jpg | Bin 0 -> 91609 bytes .../main/res/drawable/img_vinda_tissue.webp | Bin 0 -> 114958 bytes .../src/main/res/drawable/img_xiaomi14pro.jpg | Bin 0 -> 15588 bytes .../app/src/main/res/drawable/img_zhxg.webp | Bin 0 -> 196464 bytes .../src/main/res/drawable/jverify_back.xml | 15 + .../app/src/main/res/drawable/jverify_bg.xml | 8 + .../main/res/drawable/jverify_login_btn.xml | 10 + .../src/main/res/drawable/jverify_logo.png | Bin 0 -> 13271 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 30 + .../app/src/main/res/values/themes.xml | 13 + .../res/xml/accessibility_service_config.xml | 9 + .../app/src/main/res/xml/backup_rules.xml | 5 + .../main/res/xml/data_extraction_rules.xml | 11 + .../main/res/xml/network_security_config.xml | 15 + .../duobibi-mock/build.gradle.kts | 8 + .../duobibi-mock/gradle.properties | 8 + .../duobibi-mock/gradle/libs.versions.toml | 60 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43504 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + duobibi(mock)Android/duobibi-mock/gradlew | 252 +++++++ duobibi(mock)Android/duobibi-mock/gradlew.bat | 94 +++ .../duobibi-mock/settings.gradle.kts | 24 + .../duobibi-server/.gitignore | 13 + duobibi(mock)backend/duobibi-server/README.md | 230 ++++++ .../duobibi-server/app/__init__.py | 0 .../duobibi-server/app/api/__init__.py | 0 .../duobibi-server/app/api/arena.py | 191 +++++ .../duobibi-server/app/api/auth.py | 187 +++++ .../duobibi-server/app/api/parse.py | 38 + .../duobibi-server/app/api/parse_image.py | 73 ++ .../duobibi-server/app/api/parse_ocr.py | 44 ++ .../duobibi-server/app/api/parse_text.py | 52 ++ .../duobibi-server/app/api/protect.py | 220 ++++++ .../duobibi-server/app/api/quick_quote.py | 189 +++++ .../duobibi-server/app/api/wish.py | 195 +++++ .../duobibi-server/app/api/worth_buy.py | 194 +++++ duobibi(mock)backend/duobibi-server/app/db.py | 82 +++ .../duobibi-server/app/extractor.py | 243 +++++++ .../duobibi-server/app/llm_client.py | 56 ++ .../duobibi-server/app/main.py | 52 ++ .../duobibi-server/app/mock_extractor.py | 180 +++++ .../duobibi-server/app/schemas.py | 165 +++++ duobibi(mock)backend/duobibi-server/data.db | Bin 0 -> 24576 bytes .../deploy/duobibi-mock.service | 22 + .../deploy/nginx/api.duobibi.com.conf | 36 + .../duobibi-server/pyproject.toml | 27 + .../duobibi-server/run-backend.ps1 | 70 ++ .../duobibi-server/tests/__init__.py | 0 .../duobibi-server/tests/test_arena_worth.py | 95 +++ .../duobibi-server/tests/test_health.py | 13 + .../duobibi-server/tests/test_parse_image.py | 198 ++++++ 198 files changed, 15533 insertions(+) create mode 100644 duobibi(mock)Android/duobibi-mock/.gitignore create mode 100644 duobibi(mock)Android/duobibi-mock/README.md create mode 100644 duobibi(mock)Android/duobibi-mock/app/build.gradle.kts create mode 100644 duobibi(mock)Android/duobibi-mock/app/libs/MiPush_SDK_Client_7_9_2-C_3rd.aar create mode 100644 duobibi(mock)Android/duobibi-mock/app/proguard-rules.pro create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/AndroidManifest.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/DuobibiApplication.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/MainActivity.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/auth/JgInit.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/auth/OneClickLogin.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/auth/PhoneBindViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/api/ArenaApi.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/api/AuthApi.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/api/OcrApi.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/api/ParseApi.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/api/ParseDto.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/api/ProtectApi.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/api/WorthBuyApi.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/db/AppDatabase.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/db/Cluster.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/db/ClusterDao.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/db/PriceAlert.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/db/PriceAlertDao.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/db/PriceRecord.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/db/PriceRecordDao.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/data/repo/PriceRepository.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/di/DatabaseModule.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/di/NetworkModule.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/floating/AccessibilityServiceHolder.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/floating/FloatingButtonView.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/floating/NodeDto.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/floating/TreeSerializer.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/heartbeat/A11yStateProbe.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/heartbeat/AppForegroundProbe.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/heartbeat/DeviceIdProvider.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/heartbeat/HeartbeatPayload.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/heartbeat/HeartbeatReceiver.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/heartbeat/HeartbeatScheduler.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/heartbeat/HeartbeatSender.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/push/MiPushBootstrap.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/push/MiPushReceiver.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/service/FloatingWindowService.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/service/PriceBotAccessibilityService.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/DbbWidgets.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/Haptic.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/HeroBanner.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/ProductImageMap.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/UiCommon.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/alert/AlertScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/alert/AlertViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/arena/ArenaCategory.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/arena/ArenaScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/arena/ArenaViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/detail/ProductDetailScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/discover/DiscoverData.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/discover/DiscoverScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/discover/DiscoverViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/discover/PriceCompareCard.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/mine/AboutScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/mine/DiagnosticsScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/mine/DiagnosticsViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/mine/MineScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/mine/MineViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/nav/AppNavHost.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/onboarding/OnboardingScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/privacy/PrivacyConsentScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/privacy/PrivacyPolicyScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/records/ClusterDetailScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/records/ClusterDetailViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/records/NewRecordScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/records/NewRecordViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/records/PriceTrendChart.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/records/RecordsScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/records/RecordsViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/theme/Color.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/theme/Theme.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/tools/BundleCalculatorScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/tools/InstallmentCalculatorScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/tools/UnitPriceCalculatorScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/watched/WatchedProductsScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/worth/PriceHistory.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/worth/WorthBuyScreen.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/ui/worth/WorthBuyViewModel.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/AlertNotifier.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/AuthPrefs.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/Constants.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/CsvExporter.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/DemoDataPrefs.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/DemoDataSeeder.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/FloatingServiceLauncher.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/ImageCompressor.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/OnboardingPrefs.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/PermissionUtil.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/PlatformSearch.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/PrivacyPrefs.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/SettingsPrefs.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/StatsCalculator.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/VendorAutoStart.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/java/com/jishisongfu/duobibi/util/WatchedPrefs.kt create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/dialog_bg.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_cat_daily.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_cat_digital.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_cat_ecommerce.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_cat_fashion.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_cat_grocery.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_cat_takeaway.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_floating_button.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_prod_capsule.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_prod_console.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_prod_default.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_prod_earbuds.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_prod_hairdryer.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/ic_prod_phone.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_airpods.png create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_beef_rice.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_dyson_hd.png create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_dyson_vacuum.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_eggs.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_estee_lauder.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_greens.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_haidilao.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_haier_cup.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_hamburger.png create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_heytea.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_ipad_air.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_iphone_blue.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_jdb.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_kfc.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_lancome_eye.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_lululemon.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_macbook.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_merries.png create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_midea_kettle.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_moutai.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_nescafe.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_nike_af1.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_philips_shaver.png create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_philips_toothbrush.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_pork_belly.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_shiseido_red.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_skii.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_squirrel.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_supor_pan.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_telunsu.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_vinda_tissue.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_xiaomi14pro.jpg create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/img_zhxg.webp create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/jverify_back.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/jverify_bg.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/jverify_login_btn.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/drawable/jverify_logo.png create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/values/colors.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/values/strings.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/values/themes.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/xml/accessibility_service_config.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/xml/backup_rules.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 duobibi(mock)Android/duobibi-mock/app/src/main/res/xml/network_security_config.xml create mode 100644 duobibi(mock)Android/duobibi-mock/build.gradle.kts create mode 100644 duobibi(mock)Android/duobibi-mock/gradle.properties create mode 100644 duobibi(mock)Android/duobibi-mock/gradle/libs.versions.toml create mode 100644 duobibi(mock)Android/duobibi-mock/gradle/wrapper/gradle-wrapper.jar create mode 100644 duobibi(mock)Android/duobibi-mock/gradle/wrapper/gradle-wrapper.properties create mode 100644 duobibi(mock)Android/duobibi-mock/gradlew create mode 100644 duobibi(mock)Android/duobibi-mock/gradlew.bat create mode 100644 duobibi(mock)Android/duobibi-mock/settings.gradle.kts create mode 100644 duobibi(mock)backend/duobibi-server/.gitignore create mode 100644 duobibi(mock)backend/duobibi-server/README.md create mode 100644 duobibi(mock)backend/duobibi-server/app/__init__.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/__init__.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/arena.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/auth.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/parse.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/parse_image.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/parse_ocr.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/parse_text.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/protect.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/quick_quote.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/wish.py create mode 100644 duobibi(mock)backend/duobibi-server/app/api/worth_buy.py create mode 100644 duobibi(mock)backend/duobibi-server/app/db.py create mode 100644 duobibi(mock)backend/duobibi-server/app/extractor.py create mode 100644 duobibi(mock)backend/duobibi-server/app/llm_client.py create mode 100644 duobibi(mock)backend/duobibi-server/app/main.py create mode 100644 duobibi(mock)backend/duobibi-server/app/mock_extractor.py create mode 100644 duobibi(mock)backend/duobibi-server/app/schemas.py create mode 100644 duobibi(mock)backend/duobibi-server/data.db create mode 100644 duobibi(mock)backend/duobibi-server/deploy/duobibi-mock.service create mode 100644 duobibi(mock)backend/duobibi-server/deploy/nginx/api.duobibi.com.conf create mode 100644 duobibi(mock)backend/duobibi-server/pyproject.toml create mode 100644 duobibi(mock)backend/duobibi-server/run-backend.ps1 create mode 100644 duobibi(mock)backend/duobibi-server/tests/__init__.py create mode 100644 duobibi(mock)backend/duobibi-server/tests/test_arena_worth.py create mode 100644 duobibi(mock)backend/duobibi-server/tests/test_health.py create mode 100644 duobibi(mock)backend/duobibi-server/tests/test_parse_image.py diff --git a/duobibi(mock)Android/duobibi-mock/.gitignore b/duobibi(mock)Android/duobibi-mock/.gitignore new file mode 100644 index 0000000..f465629 --- /dev/null +++ b/duobibi(mock)Android/duobibi-mock/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle/ +local.properties +.idea/ +.DS_Store +build/ +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +*.aab +*.keystore +*.jks +release/ + +# 本地调试/截图/日志副产品,不进仓库(只指根目录,不递归 ignore app/src/main/java/.../data/*) +/data/ diff --git a/duobibi(mock)Android/duobibi-mock/README.md b/duobibi(mock)Android/duobibi-mock/README.md new file mode 100644 index 0000000..ff5f83d --- /dev/null +++ b/duobibi(mock)Android/duobibi-mock/README.md @@ -0,0 +1,166 @@ +# duobibi-android + +多比比(占坑版)Android 客户端。 + +跨平台比价工具:输入商品名,在「比价擂台」里把 淘宝 / 京东 / 拼多多 / 抖音 的到手价 +拉到一张表对比、自动标最低;另配「降价雷达」盯价提醒与「值不值得买」AI 评分。 +也保留在 7 个购物 App 内的悬浮「比价」按钮一键记价(本地存储,喂给擂台)。 + +> **占坑期**:与姊妹品牌「傻瓜比价」复用同一套底层引擎(无障碍+浮窗、Room、Retrofit、 +> 心跳、推送、一键登录),但包名 / 签名 / 配色 / 导航 / 文案 / 功能组合全部独立重做, +> 通过应用市场"套壳/重复"检测与"功能过于简单"审核。 + +## 技术栈 + +- Kotlin 2.1 + Jetpack Compose(Material3,BOM 2024.12) +- minSdk 26 / targetSdk 34 / compileSdk 36 +- JDK 17(`sourceCompatibility = VERSION_17`) +- Room + Hilt + Retrofit(kotlinx.serialization)+ OkHttp +- AGP 8.9 / Gradle Wrapper 内置 +- 包名 `com.jishisongfu.duobibi`,版本 `1.0.0` (versionCode 1) +- 小米推送 SDK 7.9.2 + 极光 jverification 3.1.7(ABI:armeabi-v7a / arm64-v8a) + +## 工程结构 + +``` +app/src/main/java/com/jishisongfu/duobibi/ +├── DuobibiApplication.kt # @HiltAndroidApp +├── MainActivity.kt # 单 Activity:隐私同意 → onboarding → AppNavHost +├── di/ # Hilt: DatabaseModule / NetworkModule +├── data/ +│ ├── db/ # Room v1: Cluster / PriceRecord / PriceAlert + DAO +│ ├── api/ # Retrofit: Parse / Ocr / Protect / Auth / Arena / WorthBuy +│ └── repo/PriceRepository.kt # 业务编排:记价入库 + 擂台 + 值不值 + 雷达查价 +├── service/ +│ ├── PriceBotAccessibilityService # 监听窗口切换 + 抓取页面控件树 +│ └── FloatingWindowService # 前台服务,承载比价浮窗(自我修复) +├── floating/ # AccessibilityServiceHolder / FloatingButtonView / TreeSerializer +├── heartbeat/ # 心跳上报(AlarmManager,占位 PoC) +├── push/ # 小米推送 bootstrap + receiver +├── auth/ # 极光一键登录(JgInit / OneClickLogin / PhoneBindViewModel) +├── ui/ +│ ├── theme/ # DbbColors(蓝青)+ DuobibiTheme +│ ├── nav/AppNavHost.kt # 5 Tab + 详情路由 +│ ├── arena/ # 比价擂台(首屏 Hero) +│ ├── alert/ # 降价雷达(想买的 / 已买的) +│ ├── worth/ # AI 值不值得买 +│ ├── records/ # 记录列表 / 商品组详情 / 趋势图 / 手动记一笔 +│ ├── mine/ # 我的:账号绑定 / 权限 / 自检 / 关于 +│ ├── onboarding/ + privacy/ # 首启引导 + 隐私同意/政策 +│ └── tools/ # 单价对比 / 凑单 / 分期真实利率 +└── util/ # Constants / *Prefs / PermissionUtil / AlertNotifier / DemoDataSeeder ... +``` + +## 底部 5 Tab + +| Tab | 内容 | +|---|---| +| 比价 | **比价擂台**:搜商品 → 多平台到手价对比表,标最低,可一键跳最低价平台搜 | +| 雷达 | **降价提醒雷达**:想买的(设心理价)/ 已买的(盯价保),降价推送提醒 | +| 值不值 | **AI 值不值得买**:输入商品+到手价 → 评分 + 建议 + 更划算时机 | +| 记录 | 浮窗 / 手动记的价格,按商品组聚合,看趋势图 | +| 我的 | 账号一键登录、权限、算价工具、自检、关于、隐私 | + +## 关键设计 + +### 比价擂台(首屏 Hero) + +搜索 → 调后端 `/arena-quote` 得各平台 AI 估到手价 + 市场常见价;若本机记录过同款, +本机真实价直接覆盖对应平台行并标「本机」,最低行高亮。 + +### 浮窗显隐 + 自我修复 + +`AccessibilityService` 监听 `TYPE_WINDOW_STATE_CHANGED`,在代码层比对 +`Constants.WHITELIST_PACKAGES`(7 个购物 App)决定浮窗显隐;`FloatingWindowService.notifyVisibility` +在服务被杀后能按白名单事件自动重启。浮窗按钮为蓝青「比价」圆钮。 + +### 无障碍只读 + +无障碍只在用户点击浮窗时被动抓取一次当前页面控件树,不做任何自动点击 / 输入 / 滑动等代操作。 +`accessibility_service_config.xml` 仅声明 `TYPE_WINDOW_STATE_CHANGED`,不申请截屏与手势权限。 + +### 降价雷达 + +`PriceAlert`(mode = WANT / BOUGHT)统一承载「想买的设心理价」与「已买的盯价保」。 +后台复用 `/track-price` 估当前价,跌破锚价即 `AlertNotifier` 本地通知。 + +### 算价工具(纯本地计算,不联网) + +- **单价对比**:g/kg/ml/L/件 统一换算成 100g·100ml·件 单价 +- **凑单计算器**:阶梯满减 300-50 / 600-100 / 1000-200 / 1500-300,提示"再凑 X 反而少花 Y" +- **分期真实利率**:二分法求月 IRR × 12,揭穿"月费率 × 12"低估的名义年化 + +### 隐私 + +首启全屏同意页(不同意即退出);价格记录默认仅存本机(Room),仅在主动记价 / 擂台 / 值不值时 +临时联网识别,不持久化。卸载即清。 + +## 配置 + +- **后端域名**:`app/build.gradle.kts` → release `BASE_URL = "https://api.duobibi.com/"`, + debug `BASE_URL = "http://127.0.0.1:8766/"`,强制 HTTPS(`usesCleartextTraffic="false"`) +- **release 签名**:不硬编码,从 `gradle.properties` / `-P` 注入 + `DUOBIBI_KEYSTORE / DUOBIBI_STORE_PASSWORD / DUOBIBI_KEY_ALIAS / DUOBIBI_KEY_PASSWORD`; + 未配置时 debug 走默认 debug.keystore,可直接 `assembleDebug` +- **三方 key**(占位,上线前到对应控制台注册「多比比」新应用替换): + `build.gradle.kts` 的 `JPUSH_APPKEY`、`push/MiPushBootstrap.kt` 的小米 AppID/Key +- **白名单包名**:`util/Constants.kt`(7 个购物 App) + +## 构建与启动 + +**环境要求** + +- JDK 17(`compileOptions.sourceCompatibility = VERSION_17`) +- Android Studio Hedgehog (2023.1) 及以上 +- Android SDK Platform 34(targetSdk)/ 36(compileSdk) +- 测试设备:Android 8.0 (API 26) 及以上 + +**1. 同步项目** + +用 Android Studio 直接 Open 本目录,首次同步会自动生成 `local.properties` 指向你的 Android SDK。 + +**2. 构建 APK** + +```bash +./gradlew :app:assembleDebug # debug 包,开箱即用 +./gradlew :app:assembleRelease # 需先配置 DUOBIBI_KEYSTORE 等签名属性 +``` + +**3. 安装到设备** + +```bash +./gradlew :app:installDebug # 编译并安装到当前连接的设备 +# 或手动安装已有 apk: +adb install app/build/outputs/apk/debug/app-debug.apk +``` + +**4. 联调本地后端(debug 模式必做)** + +debug 包的 `BASE_URL` 为 `http://127.0.0.1:8766/`(见 `app/build.gradle.kts`),需先启动后端,再把设备的 8766 反向映射到 PC: + +```bash +adb reverse tcp:8766 tcp:8766 +``` + +release 包直接走 `https://api.duobibi.com/`,无需此步。 + +## 真机调试关键步骤 + +安装后,首次打开 App: + +1. 阅读并同意隐私 → 看 4 屏引导 +2. 进入「我的」Tab,挨个开三项权限: + - 无障碍服务 → 系统设置 → 已下载的服务 → 找到「多比比」→ 开启 + - 悬浮窗 → 系统授权页直接开 + - 自启动 → 跳厂商自启动管理页(识别失败有 toast 提示手动) +3. 「比价」Tab 输入商品名 → 看多平台对比表 +4. 切到 淘宝 / 京东 / 拼多多 / 抖音 / 美团 / 美团外卖 / 饿了么 任意商品页 → + 点蓝色「比价」浮窗 → toast「已记 ¥XX(平台)」→ 回 App「记录」Tab 查看,并自动进入擂台对比 +5. 「雷达」加一条想买的设心理价;「值不值」输入商品+价格看 AI 评分 + +## 待办(占坑期遗留) + +- 三方 key(极光 / 小米 AppKey、智谱 LLM)需注册「多比比」新应用后替换占位值 +- release 签名属性 `DUOBIBI_KEYSTORE` 未配置时 release 无签名(debug 不受影响) +- 心跳 `HEARTBEAT_BASE_URL` 仍是局域网占位 IP(`192.168.0.109:8766`),生产化需改真后端 https +- 极光一键登录在控制台按"包名+签名 MD5"校验,真机联调登录请用配置了 `DUOBIBI_KEYSTORE` 的 release 包 diff --git a/duobibi(mock)Android/duobibi-mock/app/build.gradle.kts b/duobibi(mock)Android/duobibi-mock/app/build.gradle.kts new file mode 100644 index 0000000..2544683 --- /dev/null +++ b/duobibi(mock)Android/duobibi-mock/app/build.gradle.kts @@ -0,0 +1,139 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) +} + +android { + namespace = "com.jishisongfu.duobibi" + compileSdk = 36 + + defaultConfig { + applicationId = "com.jishisongfu.duobibi" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0.0" + + vectorDrawables { useSupportLibrary = true } + + // 极光一键登录 / 运营商 SDK 含 native 库,只保留主流 ABI + ndk { + abiFilters += listOf("armeabi-v7a", "arm64-v8a") + } + + // 极光 AppKey 通过 manifest 占位符注入(SDK 校验 包名+签名+AppKey 三元组)。 + // 占位值,需在极光控制台注册"多比比"新应用后替换。 + manifestPlaceholders["JPUSH_APPKEY"] = "966b451a8d9cfe12d173ea9d" + manifestPlaceholders["JPUSH_CHANNEL"] = "developer-default" + } + + signingConfigs { + create("release") { + // 多比比自己的 keystore:从 ~/.gradle/gradle.properties 或 -P 注入,不硬编码路径/密码。 + // 未提供时 release 不签名;debug 走默认 debug.keystore,可直接 assembleDebug。 + val ksPath = (project.findProperty("DUOBIBI_KEYSTORE") as String?).orEmpty() + if (ksPath.isNotEmpty() && file(ksPath).exists()) { + storeFile = file(ksPath) + storePassword = (project.findProperty("DUOBIBI_STORE_PASSWORD") as String?).orEmpty() + keyAlias = (project.findProperty("DUOBIBI_KEY_ALIAS") as String?).orEmpty() + keyPassword = (project.findProperty("DUOBIBI_KEY_PASSWORD") as String?).orEmpty() + // 4 大国产商店对 V1+V2 双签兼容性最好;V3 由 AGP 默认处理 + enableV1Signing = true + enableV2Signing = true + } + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + buildConfigField("String", "BASE_URL", "\"https://api.duobibi.com/\"") + // 心跳 PoC mock 后端(本机 LAN),独立端口 8766。生产化改真后端 https。 + buildConfigField("String", "HEARTBEAT_BASE_URL", "\"http://192.168.0.109:8766\"") + if ((project.findProperty("DUOBIBI_KEYSTORE") as String?)?.isNotEmpty() == true) { + signingConfig = signingConfigs.getByName("release") + } + } + debug { + isMinifyEnabled = false + // 本机联调:debug 走 loopback,需先在宿主执行 `adb reverse tcp:8766 tcp:8766` + // 把模拟器内 127.0.0.1:8766 反向映射到宿主 8766。 + // 为什么不直接用 10.0.2.2:模拟器 IPv6 dual-stack 默认走 IPv6 出站,但模拟器 + // 内核 IPv6 路由不通,直接 ENETUNREACH (force-stop 后必现)。走 loopback 绕开。 + // 线上/release 仍为 https://api.duobibi.com/(见上方 release 块)。 + buildConfigField("String", "BASE_URL", "\"http://127.0.0.1:8766/\"") + buildConfigField("String", "HEARTBEAT_BASE_URL", "\"http://192.168.0.109:8766\"") + // debug 用默认 debug.keystore,可直接 assembleDebug 出包。 + // 一键登录在极光控制台按"包名+签名 MD5"校验,真机联调登录请用配置了 + // DUOBIBI_KEYSTORE 的 release 包(签名 MD5 需与控制台一致)。 + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + buildConfig = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.activity.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.navigation.compose) + debugImplementation(libs.androidx.compose.ui.tooling) + + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + implementation(libs.androidx.hilt.navigation.compose) + + implementation(libs.retrofit) + implementation(libs.retrofit.kotlinx.serialization) + implementation(libs.okhttp) + implementation(libs.kotlinx.serialization.json) + + implementation(libs.kotlinx.coroutines.android) + + // 小米推送 SDK 7.9.2 (C 3rd)。aar 内置 manifest 自动 merge 全部组件 + 权限, + // 业务方仅需注册自家 PushMessageReceiver 子类(见 push/MiPushReceiver.kt)。 + implementation(files("libs/MiPush_SDK_Client_7_9_2-C_3rd.aar")) + + // 极光一键登录(jverification 3.0+ 自带 JCore) + implementation("cn.jiguang.sdk:jverification:3.1.7") +} diff --git a/duobibi(mock)Android/duobibi-mock/app/libs/MiPush_SDK_Client_7_9_2-C_3rd.aar b/duobibi(mock)Android/duobibi-mock/app/libs/MiPush_SDK_Client_7_9_2-C_3rd.aar new file mode 100644 index 0000000000000000000000000000000000000000..00d45a473903e52f2ec0f6f5c664514acbdec53c GIT binary patch literal 1008221 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