{"id":1315,"date":"2026-09-14T15:30:12","date_gmt":"2026-09-14T15:30:12","guid":{"rendered":"https:\/\/boochlin.com\/?p=1315"},"modified":"2026-09-14T15:57:28","modified_gmt":"2026-09-14T15:57:28","slug":"funny-vote-app-%e5%be%9e%e9%9b%b6%e9%96%8b%e5%a7%8b%e7%9a%84-vibe-coding-%e5%af%a6%e6%88%b0%e6%8c%87%e5%8d%97%ef%bc%9a%e4%bb%a5-codex-%e8%a6%96%e8%a7%92%e7%9c%8b-ai-%e5%8d%94%e5%90%8c","status":"publish","type":"post","link":"https:\/\/boochlin.com\/?p=1315","title":{"rendered":"Funny vote APP \u2013 \u5f9e\u96f6\u958b\u59cb\u7684 Vibe Coding \u5be6\u6230\u6307\u5357\uff1a\u4ee5 Codex \u8996\u89d2\u770b AI \u5354\u540c\u3001\u7ffb\u8eca\u73fe\u5834\u8207\u8cc7\u5b89\u52a0\u56fa\uff08\u4e03\u00b7\u7d42\u5c40\u7bc7\uff09"},"content":{"rendered":"<p>\u5728\u524d\u9762\u516d\u7bc7\u6f2b\u9577\u7684\u6f14\u9032\u96dc\u8a18\u88e1\uff0c\u6211\u5011\u4e00\u8def\u5f9e 2016 \u5e74\u624b\u6572 ButterKnife \u7684\u820a\u77f3\u5668\u6642\u4ee3\uff0c\u804a\u5230 2018 \u5e74 Kotlin 1.2 \u7684\u9072\u4f86\u6551\u8d16\uff1b\u5728 2024 \u5e74\u649e\u4e0a Compose \u91cd\u7d44\u8207\u72c0\u614b\u6495\u88c2\u5f8c\u76f4\u4e0a MVI\uff1b\u518d\u5230 2026 \u5e74\u82b1\u8cbb\u5927\u91cf\u9ad4\u529b\u6d3b\u7121\u640d\u5fa9\u523b 100% \u756b\u9762\u3001\u4e26\u7528 Firebase \u70ba\u90a3\u53f0\u65e9\u5c31\u6b7b\u900f\u7684\u4e94\u7f8e\u5143\u4f3a\u670d\u5668\u63db\u4e0a\u4e00\u9846\u96f2\u539f\u751f\u5fc3\u81df\u3002<\/p>\n<p>\u770b\u8457\u5341\u5e74\u524d\u5beb\u4e0b\u7684\u67b6\u69cb\u4ecb\u9762\uff0c\u5728\u63db\u4e0a\u73fe\u4ee3\u8cc7\u6599\u7ba1\u7dda\u5f8c\u7adf\u7136\u80fd\u7121\u7e2b\u9023\u4e0a\u96f2\u7aef\u958b\u7968\uff0c\u7576\u4e0b\u78ba\u5be6\u723d\u611f\u5341\u8db3\u3002\u4f46\u51b7\u975c\u4e0b\u4f86\u5f8c\uff0c\u6211\u4e0d\u7981\u6d6e\u73fe\u51fa\u4e00\u500b\u8eab\u70ba\u73fe\u4ee3\u5de5\u7a0b\u5e2b\u7684\u7d42\u6975\u5047\u8a2d\uff1a<\/p>\n<p><strong>\u5982\u679c\u4eca\u5929\u6211\u5011\u4e0d\u662f\u5728\u7d66\u5341\u5e74\u524d\u7684\u6b77\u53f2\u5305\u88b1\u64e6\u5c41\u80a1\uff0c\u800c\u662f\u628a\u6642\u9593\u8ef8\u76f4\u63a5\u62c9\u56de\u73fe\u5728\uff0c\u5f9e\u4e00\u500b\u4e7e\u4e7e\u6de8\u6de8\u7684\u76ee\u9304\u6572\u4e0b git init\uff0c\u91cd\u65b0\u505a\u4e00\u6b21 FunnyVote\uff0c\u4e3b\u6d41\u5230\u5e95\u8a72\u600e\u9ebc\u73a9\u73fe\u5728\u6700\u706b\u71b1\u7684 Vibe Coding\uff1f<\/strong><\/p>\n<p>\u9019\u5e74\u982d\u5728\u793e\u7fa4\u5a92\u9ad4\u4e0a\uff0c\u6bcf\u500b\u4eba\u958b\u53e3\u9589\u53e3\u90fd\u5728\u5439\u6367 Vibe Coding\uff0c\u5f77\u5f7f\u5de5\u7a0b\u5e2b\u53ea\u8981\u7ff9\u8457\u4e8c\u90ce\u817f\u5728\u5c0d\u8a71\u6846\u88e1\u96a8\u4fbf\u6572\u5169\u53e5\u300c\u5e6b\u6211\u5beb\u500b\u6295\u7968 App\u300d\uff0cAI \u5c31\u6703\u81ea\u52d5\u901a\u9748\u51fa\u5b8c\u7f8e\u7684\u751f\u7522\u74b0\u5883\u67b6\u69cb\u3002<\/p>\n<p>\u99ac\u7684\uff0c\u9019\u7a2e\u9b3c\u8a71\u807d\u807d\u5c31\u597d\u3002\u53ea\u8981\u4f60\u5728\u7b2c\u4e00\u7dda\u5e36\u904e\u5be6\u7fd2\u751f\uff0c\u6216\u8005\u89aa\u624b\u6aa2\u67e5\u904e AI \u5410\u51fa\u4f86\u7684\u4ee3\u78bc\uff0c\u4f60\u5c31\u6703\u77e5\u9053\uff1a\u76f2\u6253 Prompt \u7684\u4e0b\u5834\uff0c\u901a\u5e38\u662f\u5f97\u5230\u4e00\u5806\u770b\u4f3c\u7de8\u8b6f\u6703\u904e\u3001\u4f46\u4e00\u4e0a\u7dda\u5c31\u8b93\u4f60\u7834\u7522\u5d29\u6f70\u7684\u6578\u4f4d\u5783\u573e\u3002<\/p>\n<p>\u300c\u6020\u60f0\u662f\u958b\u767c\u7684\u6e90\u982d\u300d\uff0c\u5c0f\u5f1f\u6211\u4e5f\u60f3\u5077\u61f6\uff0c\u4e5f\u60f3\u559d\u8457\u5496\u5561\u770b\u7a0b\u5f0f\u81ea\u5df1\u751f\u51fa\u4f86\u3002\u4f46\u8981\u6020\u60f0\u5f97\u512a\u96c5\u3001\u6020\u60f0\u5f97\u665a\u4e0a\u7761\u5f97\u8457\u89ba\uff0c\u4f60\u4e0d\u80fd\u9760\u7948\u79b1\u3002\u4eca\u5929\u9019\u7bc7\u7cfb\u5217\u7d42\u5c40\u7bc7\uff0c\u5c31\u5f9e\u5c0f\u5f1f\u8eab\u908a\u90a3\u4f4d\u51b7\u9762\u5be9\u67e5\u54e1 Codex \u7684\u8996\u89d2\uff0c\u4f86\u804a\u804a\u73fe\u4ee3\u67b6\u69cb\u5e2b\u5230\u5e95\u8a72\u5982\u4f55\u7528\u300c\u8b77\u6b04\u300d\u628a\u72c2\u98c6\u7684 AI \u97c1\u7e69\u62c9\u4f4f\uff0c\u4ee5\u53ca\u6211\u5011\u5728\u9019\u6b21\u7d50\u5c0d\u904e\u7a0b\u4e2d\u6293\u5305\u7684\u90a3\u4e9b\u81f4\u547d\u8cc7\u5b89\u5730\u96f7\u3002<\/p>\n<hr \/>\n<h2>\u4e00\u3001 \u4e3b\u6d41 Vibe Coding \u7684\u6b98\u9177\u771f\u76f8\uff1a\u8b77\u6b04\u9a45\u52d5\u958b\u767c\uff08Harness-Driven Development\uff09<\/h2>\n<p>\u70ba\u4ec0\u9ebc\u958b\u500b\u804a\u5929\u8996\u7a97\u76f2\u6253 Prompt\uff0c\u5beb\u51fa\u4f86\u7684\u4e5d\u6210\u4e5d\u662f\u5783\u573e\uff1f<\/p>\n<p>AI \u7684\u672c\u8cea\u662f\u500b\u6a5f\u7387\u9810\u6e2c\u6a21\u578b\u3002\u7576\u4f60\u5c0d\u5b83\u4e0b\u9054\u4e00\u500b\u542b\u7cca\u7684\u6307\u4ee4\uff1a\u300c\u5e6b\u6211\u5beb\u4e00\u500b Android \u6295\u7968\u8a73\u60c5\u9801\uff0c\u8981\u63a5 Firebase Firestore\u300d\uff0c\u5b83\u8166\u888b\u88e1\u6d6e\u73fe\u7684\u4e0d\u662f\u751f\u7522\u74b0\u5883\u7684\u9ad8\u53ef\u7528\u6027\u67b6\u69cb\uff0c\u800c\u662f\u7db2\u8def\u4e0a\u6210\u5343\u4e0a\u842c\u7bc7\u70ba\u4e86\u9a19\u9ede\u95b1\u3001\u628a\u6240\u6709\u908a\u754c\u689d\u4ef6\u8207\u9632\u79a6\u6a5f\u5236\u95b9\u5272\u5149\u7684\u5165\u9580\u6559\u5b78\u6587\u3002<\/p>\n<p>\u65bc\u662f\uff0c\u5b83\u6703\u5728\u4e09\u79d2\u5167\u72c2\u66b4\u7522\u51fa\u4ee5\u4e0b\u5091\u4f5c\uff1a<\/p>\n<ol>\n<li><strong>\u72c0\u614b\u6eff\u5730\u722c<\/strong>\uff1a\u5728 ViewModel \u88e1\u96a8\u624b\u958b\u4e94\u500b\u7368\u7acb\u7684 <code>MutableStateFlow<\/code>\uff0c\u8b93\u4f60\u7684 Compose \u756b\u9762\u9583\u720d\u8df3\u52d5\uff08\u72c0\u614b\u6495\u88c2\uff09\u3002<\/li>\n<li><strong>\u5b89\u5168\u9580\u5927\u958b<\/strong>\uff1a\u5728 Firestore \u5b89\u5168\u898f\u5247\u96a8\u624b\u88dc\u4e0a\u4e00\u53e5 <code>allow read, write: if true;<\/code>\uff0c\u628a\u4f60\u7684\u8cc7\u6599\u5eab\u76f4\u63a5\u88f8\u5954\u7d66\u5168\u4e16\u754c\u3002<\/li>\n<li><strong>\u8a9e\u7fa9\u578b\u5225\u6d88\u5931<\/strong>\uff1a\u5230\u8655\u5145\u65a5\u8457 String \u62fc\u8cbc\u8207 <code>Map&lt;String, Any&gt;<\/code>\uff0c\u9023\u500b\u56b4\u683c\u7684\u578b\u5225\u7d04\u675f\u90fd\u6c92\u6709\u3002<\/li>\n<\/ol>\n<p>\u9019\u4e0d\u53eb Vibe Coding\uff0c\u9019\u53eb\u7d66\u672a\u4f86\u7684\u81ea\u5df1\u57cb\u68fa\u6750\u3002<\/p>\n<p>\u771f\u6b63\u7684\u73fe\u4ee3 Vibe Coding\uff0c\u6838\u5fc3\u601d\u60f3\u53ea\u6709\u516b\u500b\u5b57\uff1a<strong>\u8b77\u6b04\u9a45\u52d5\u958b\u767c\uff08Harness-Driven Development\uff09<\/strong>\u3002<\/p>\n<p>\u5728\u8b93 AI \u6572\u4e0b\u7b2c\u4e00\u884c\u696d\u52d9\u908f\u8f2f\u4e4b\u524d\uff0c\u4eba\u985e\u67b6\u69cb\u5e2b\u7684\u6838\u5fc3\u4efb\u52d9\u4e0d\u662f\u6572\u9375\u76e4\uff0c\u800c\u662f\u66ff\u5b83\u642d\u5efa\u8dd1\u9053\u8207\u9632\u649e\u8b77\u6b04\uff1a<\/p>\n<ul>\n<li><strong>\u4e0a\u4e0b\u6587\u5de5\u7a0b\uff08Context Engineering\uff09<\/strong>\uff1a\u4e0d\u8981\u628a\u6574\u5ea7\u4ee3\u78bc\u5eab\u7121\u8166\u4e1f\u7d66\u5b83\u3002\u53ea\u7d66\u5b83\u7cbe\u78ba\u7684\u9818\u57df\u6a21\u578b\uff08Domain Model\uff09\u8207\u5951\u7d04\u4ecb\u9762\uff08Contract\uff09\uff0c\u5207\u65b7\u6240\u6709\u5916\u90e8\u5e72\u64fe\u3002<\/li>\n<li><strong>\u67b6\u69cb\u5148\u884c\uff08Schema-First\uff09<\/strong>\uff1a\u8cc7\u6599\u5eab\u7684\u6b04\u4f4d\u898f\u683c\u3001\u4e0d\u53ef\u8b8a\u7d04\u675f\u8207\u9a57\u8b49\u908a\u754c\uff0c\u5fc5\u9808\u7531\u4eba\u985e\u89aa\u81ea\u62cd\u677f\uff0c\u4e0d\u80fd\u8b93 AI \u81ea\u4f5c\u4e3b\u5f35\u3002<\/li>\n<li><strong>\u578b\u5225\u97c1\u7e69\uff08Type Constraints\uff09<\/strong>\uff1a\u5229\u7528 Kotlin \u7684 Sealed Interface\u3001Value Class \u8207\u56b4\u683c Data Class\uff0c\u628a\u72c0\u614b\u6d41\u8f49\u7684\u6bcf\u4e00\u6b65\u6b7b\u6b7b\u9650\u7e2e\u5728\u7de8\u8b6f\u5668\u7684\u6aa2\u67e5\u7bc4\u570d\u5167\u3002<\/li>\n<\/ul>\n<p>\u7576\u8b77\u6b04\u67b6\u8a2d\u5b8c\u7562\uff0cAI \u7684\u6975\u9650\u624b\u901f\u624d\u6703\u8b8a\u6210\u4f60\u7684\u8d85\u7d1a\u5916\u639b\uff1b\u5426\u5247\uff0c\u5b83\u53ea\u662f\u4e00\u8f1b\u6c92\u6709\u715e\u8eca\u3001\u76f4\u885d\u61f8\u5d16\u7684\u8d85\u8dd1\u3002<\/p>\n<hr \/>\n<h2>\u4e8c\u3001 \u5782\u76f4\u5207\u7247\uff08Vertical Slice\uff09\u5be6\u6230\uff1a\u4eba\u985e\u7684 Prompt \u8207 AI \u7684\u521d\u7248\u7ffb\u8eca\u4ee3\u78bc<\/h2>\n<p>\u5f88\u591a\u958b\u6e90\u5c08\u6848\u91cd\u69cb\u5931\u6557\u7684\u539f\u56e0\uff0c\u90fd\u662f\u4e00\u958b\u59cb\u597d\u5927\u559c\u529f\uff0c\u60f3\u8457\u7b2c\u4e00\u5929\u5c31\u8981\u628a\u7db2\u8def\u5c64\u3001\u5feb\u53d6\u5c64\u3001\u6240\u6709\u5206\u9801\u4e00\u6b21\u523b\u5b8c\u3002\u4eba\u53ea\u8981\u4e00\u8caa\u5fc3\uff0c\u4ee3\u78bc\u5c31\u6703\u5931\u63a7\u3002\u6b63\u78ba\u7684\u505a\u6cd5\u662f\u300c\u5782\u76f4\u5207\u7247\uff08Vertical Slice\uff09\u300d\u2014\u2014\u6293\u51fa\u4e00\u689d\u6700\u6838\u5fc3\u7684\u95dc\u9375\u8def\u5f91\uff08Critical User Journey\uff09\uff0c\u5f9e UI\u3001ViewModel\u3001Repository \u4e00\u8def\u8cab\u7a7f\u5230\u5e95\u5c64\u8cc7\u6599\u5eab\u3002<\/p>\n<p>\u5728 FunnyVote \u4e2d\uff0c\u9019\u689d\u8def\u5f91\u975e\u5e38\u7d14\u7cb9\uff1a\u9996\u9801\u52a0\u8f09\u71b1\u9580\u6295\u7968\u6e05\u55ae\u3002<\/p>\n<p><strong>[\u4eba\u985e\u67b6\u69cb\u5e2b\u4e0b\u9054\u7684\u7cbe\u78ba Prompt]<\/strong>\uff1a<\/p>\n<pre><code>\u8acb\u4f9d\u64da\u5c08\u6848\u73fe\u6709\u7684 MVI \u67b6\u69cb\u898f\u7bc4\uff0c\u5be6\u4f5c FunnyVote \u9996\u9801\u6295\u7968\u6e05\u55ae\u7684\u5782\u76f4\u5207\u7247\u3002\n\u8981\u6c42\uff1a\n1. \u56b4\u683c\u55ae\u5411\u8cc7\u6599\u6d41\uff08UDF\uff09\uff1a\u9996\u9801\u50c5\u5141\u8a31\u66b4\u9732\u55ae\u4e00\u4e0d\u53ef\u8b8a HomeUiState\uff08\u4f7f\u7528 Data Class\uff0c\u56b4\u7981\u5c07\u756b\u9762\u6574\u9ad4\u72c0\u614b\u5ba3\u544a\u70ba\u591a\u500b\u7368\u7acb StateFlow\uff09\u3002\n2. \u6240\u6709\u4f7f\u7528\u8005\u64cd\u4f5c\u5c01\u88dd\u70ba\u5f37\u578b\u5225 HomeIntent\u3002\n3. \u8cc7\u6599\u5c64\u5fc5\u9808\u9075\u5b88\u4f9d\u8cf4\u53cd\u8f49\uff1aViewModel \u50c5\u4f9d\u8cf4 VoteRepository\u3002\n4. \u8cc7\u6599\u7b56\u7565\uff1a\u4ee5 Remote \u70ba\u706b\u8eca\u982d\uff0c\u4f46\u7576\u96f2\u7aef\u903e\u6642\u6216\u65b7\u7db2\u6642\uff0c\u5fc5\u9808\u900f\u904e Flow catch \u64cd\u4f5c\u7b26\u7121\u7e2b\u964d\u7d1a\u81f3 Room voteDao \u65e2\u6709\u7684\u672c\u6a5f\u8cc7\u6599\uff0c\u78ba\u4fdd\u65b7\u7db2\u4e0d\u767d\u5c4f\u3002<\/code><\/pre>\n<p>\u770b\u8d77\u4f86\u6307\u793a\u5f88\u660e\u78ba\u5c0d\u5427\uff1f\u4f46\u7576\u6211\u5011\u628a\u9019\u6bb5\u8a71\u4e1f\u7d66 AI\uff0c\u770b\u5b83\u7b2c\u4e00\u6b21\u5410\u51fa\u4f86\u7684\u4ee3\u78bc\uff0c\u8eab\u70ba\u5be9\u67e5\u54e1\u7684\u6211\u4e0d\u7981\u5012\u5438\u4e00\u53e3\u6dbc\u6c23\u3002<\/p>\n<h3>\u7ffb\u8eca\u73fe\u5834\u4e00\uff1aAI \u523b\u51fa\u7684\u96d9\u5411\u5373\u6642\u540c\u6b65\u602a\u7269<\/h3>\n<p>AI \u6c92\u6709\u4e56\u4e56\u5beb\u51fa\u7c21\u55ae\u7684\u964d\u7d1a\u908f\u8f2f\uff0c\u53cd\u800c\u81ea\u4f5c\u8070\u660e\u642c\u51fa\u4e86\u6559\u79d1\u66f8\u4e0a\u6975\u5176\u80a5\u5927\u7684 NetworkBoundResource \u6a21\u5f0f\uff1a<\/p>\n<pre><code class=\"language-kotlin\">\/\/ AI \u7b2c\u4e00\u6b21\u7d66\u51fa\u7684\u5929\u771f\u4ee3\u78bc\uff1a\u8a66\u5716\u5728\u5ba2\u6236\u7aef\u641e\u96d9\u5411\u8cc7\u6599\u540c\u6b65\u7684\u60e1\u5922\nclass VoteRepository @Inject constructor(\n    private val voteDao: VoteDao,\n    private val firestore: FirebaseFirestore\n) {\n    fun getAllVotes(): Flow&lt;List&lt;VoteWithDetails&gt;&gt; = flow {\n        \/\/ \u5148\u5410\u51fa\u672c\u5730\u820a\u8cc7\u6599\n        val localData = voteDao.getAllVotes().first()\n        emit(localData)\n        \n        \/\/ \u5929\u771f\u5730\u639b\u4e0a Firestore \u5373\u6642\u76e3\u807d\uff0c\u4e26\u8a66\u5716\u6bcf\u6b21\u8b8a\u52d5\u90fd\u5beb\u56de Room\n        firestore.collection(\"polls\").addSnapshotListener { snapshot, error -&gt;\n            if (snapshot != null) {\n                val remoteVotes = snapshot.toObjects(VoteEntity::class.java)\n                CoroutineScope(Dispatchers.IO).launch {\n                    voteDao.insertAll(remoteVotes) \/\/ \u78c1\u789f I\/O \u72c2\u98c6\uff0c\u4e14\u5f15\u767c\u7121\u9650\u91cd\u7e6a\u8ff4\u5708\n                }\n            }\n        }\n        \/\/ \u63a5\u8457\u76e3\u807d Room \u7684\u8b8a\u52d5\u5f80\u4e0b\u6e38\u9001\n        emitAll(voteDao.getAllVotes())\n    }\n}<\/code><\/pre>\n<p>\u9019\u6bb5\u4ee3\u78bc\u7de8\u8b6f\u5b8c\u5168\u6b63\u5e38\uff0c\u8dd1\u8d77\u4f86\u4f3c\u4e4e\u4e5f\u770b\u5f97\u5230\u8cc7\u6599\u3002\u4f46\u53ea\u8981\u7a0d\u5fae\u61c2\u4e00\u9ede\u67b6\u69cb\u7684\u4eba\u5c31\u80fd\u770b\u51fa\u88e1\u9762\u7684\u81f4\u547d\u786c\u50b7\uff1a<\/p>\n<ol>\n<li><strong>\u5354\u7a0b\u6d29\u6f0f<\/strong>\uff1a\u5728 Repository \u5167\u90e8\u96a8\u624b new \u4e86\u4e00\u500b\u4e0d\u53d7\u751f\u547d\u9031\u671f\u63a7\u7ba1\u7684 <code>CoroutineScope<\/code>\uff0c\u756b\u9762\u95dc\u9589\u5f8c\u80cc\u666f\u5beb\u5165\u4f9d\u7136\u5728\u8dd1\u3002<\/li>\n<li><strong>\u96d9\u5411\u540c\u6b65\u6b7b\u5faa\u74b0<\/strong>\uff1aFirestore \u63a8\u9001\u89f8\u767c Room \u5beb\u5165\uff0cRoom \u5beb\u5165\u89f8\u767c DAO Flow \u767c\u5c04\uff0c\u82e5\u9019\u6642\u53c8\u89f8\u767c\u4e86\u67d0\u500b\u72c0\u614b\u66f4\u65b0\uff0c\u624b\u6a5f\u76f4\u63a5\u5728\u80cc\u666f\u8b8a\u6210\u6696\u624b\u5bf6\u3002<\/li>\n<li><strong>\u76e3\u807d\u5668\u6c38\u9060\u4e0d\u91cb\u653e<\/strong>\uff1a<code>addSnapshotListener<\/code> \u6c92\u6709\u88ab\u4efb\u4f55 <code>Disposable<\/code> \u6216 <code>awaitClose<\/code> \u7ba1\u7406\uff0c\u4f7f\u7528\u8005\u9032\u51fa\u9996\u9801\u5e7e\u6b21\uff0c\u8a18\u61b6\u9ad4\u88e1\u5c31\u98c4\u8457\u5e7e\u5341\u500b\u5b64\u9b42\u91ce\u9b3c\u9023\u7dda\u3002<\/li>\n<\/ol>\n<h3>\u7ffb\u8eca\u73fe\u5834\u4e8c\uff1aAI \u7b2c\u4e8c\u7248\u7559\u4e0b\u7684\u300c\u5047\u96e2\u7dda\u512a\u5148\uff08Fake Offline-First\uff09\u300d\u7834\u7dbb<\/h3>\n<p>\u7576\u6211\u75db\u7f75\u4e86 AI\u4e00\u9813\uff0c\u53eb\u5b83\u628a\u96d9\u5411\u540c\u6b65\u780d\u6389\u5f8c\uff0c\u5b83\u4ea4\u51fa\u4e86\u7b2c\u4e8c\u7248\uff1a<\/p>\n<pre><code class=\"language-kotlin\">\/\/ AI \u7b2c\u4e8c\u7248\u5beb\u6cd5\uff1a\u8868\u9762\u770b\u4f3c\u6b63\u5e38\uff0c\u5be6\u5247\u7834\u58de\u4e86\u55ae\u4e00\u6578\u64da\u6e90\uff08SSOT\uff09\nfun getAllVotes(): Flow&lt;List&lt;VoteWithDetails&gt;&gt; = flow {\n    remoteDataSource.getAllVotes()\n        .catch { e -&gt; emitAll(voteDao.getAllVotes().onStart { checkAndSeedData() }) }\n        .collect { remoteVotes -&gt;\n            voteDao.syncVotes(remoteVotes)\n            emit(remoteVotes) \/\/ \u81f4\u547d\u6b7b\u89d2\uff1a\u76f4\u63a5\u628a\u9060\u7aef\u8cc7\u6599\u767c\u5c04\u7d66 UI\uff01\n        }\n}<\/code><\/pre>\n<p>\u9019\u6bb5\u4ee3\u78bc\u6975\u5ea6\u96b1\u853d\uff0c\u9023\u5f88\u591a\u8cc7\u6df1\u5de5\u7a0b\u5e2b\u4e00\u773c\u770b\u904e\u53bb\u90fd\u6703\u88ab\u9a19\u904e\uff1a<\/p>\n<p>\u300c\u9019\u4e0d\u662f\u6709 catch \u964d\u7d1a\u3001\u6709 syncVotes \u540c\u6b65\u55ce\uff1f\u54ea\u88e1\u6709\u554f\u984c\uff1f\u300d<\/p>\n<p><strong>\u554f\u984c\u51fa\u5728 <code>emit(remoteVotes)<\/code>\uff01<\/strong><\/p>\n<p>\u5728\u7db2\u8def\u6b63\u5e38\u7684\u60c5\u6cc1\u4e0b\uff0cUI \u6536\u96c6\u5230\u7684\u662f\u76f4\u63a5\u4f86\u81ea\u96f2\u7aef\u9060\u7aef\u7684 <code>remoteVotes<\/code>\uff0c\u800c\u4e0d\u662f Room \u672c\u5730\u8cc7\u6599\u5eab\u7684 Flow\uff01<br \/>\n\u9019\u6703\u5c0e\u81f4\u4ec0\u9ebc\u707d\u96e3\uff1f\u5982\u679c\u4f7f\u7528\u8005\u5728\u6295\u7968\u5361\u7247\u4e0a\u9ede\u4e86\u300c\u6536\u85cf\uff08Favorite\uff09\u300d\u6216\u5728\u96e2\u7dda\u6642\u6295\u4e86\u7968\uff0c\u672c\u5730 Room \u8cc7\u6599\u5eab\u96d6\u7136\u66f4\u65b0\u4e86\uff0c<strong>\u4f46\u56e0\u70ba UI \u7576\u524d\u8a02\u95b1\u7684\u662f\u9060\u7aef\u6d41\uff0c\u756b\u9762\u6839\u672c\u4e0d\u6703\u5237\u65b0\uff01<\/strong>\u4f7f\u7528\u8005\u5fc5\u9808\u7b49\u5230\u4e0b\u4e00\u6b21\u7db2\u8def\u91cd\u65b0\u8acb\u6c42\uff0c\u6536\u85cf\u5716\u6a19\u624d\u6703\u4eae\u8d77\u3002\u9019\u5c31\u662f\u5178\u578b\u7684\u300c\u5047\u96e2\u7dda\u512a\u5148\u300d\u2014\u2014\u5b83\u7834\u58de\u4e86<strong>\u55ae\u4e00\u771f\u5be6\u6578\u64da\u6e90\uff08Single Source of Truth, SSOT\uff09<\/strong>\u539f\u5247\uff01<\/p>\n<h3>\u4eba\u985e\u67b6\u69cb\u5e2b\u7684\u771f\u6b63 SSOT \u65b9\u6848\uff1aRoom \u4f5c\u70ba\u552f\u4e00\u6578\u64da\u6e90<\/h3>\n<p>\u8eab\u70ba\u67b6\u69cb\u5e2b\uff0c\u6211\u89aa\u624b\u91cd\u69cb\u4e86\u771f\u6b63\u7684\u96e2\u7dda\u512a\u5148\u55ae\u5411\u6d41\uff1a<\/p>\n<pre><code class=\"language-kotlin\">\/\/ \u4eba\u985e\u67b6\u69cb\u5e2b\u7684\u7d42\u6975\u6536\u6582\uff1aUI \u6c38\u9060\u53ea\u770b Room\uff0c\u7db2\u8def\u53ea\u8ca0\u8cac\u55ae\u5411\u540c\u6b65\n@Singleton\nclass VoteRepository @Inject constructor(\n    private val voteDao: VoteDao,\n    private val remoteDataSource: VoteRemoteDataSource,\n    @ApplicationScope private val externalScope: CoroutineScope\n) {\n    \/\/ \u552f\u4e00\u7684\u6578\u64da\u51fa\u53e3\uff1aUI \u6c38\u9060\u4e14\u53ea\u8a02\u95b1\u672c\u5730 Room \u8cc7\u6599\u5eab\uff01\n    fun getAllVotes(): Flow&lt;List&lt;VoteWithDetails&gt;&gt; = voteDao.getAllVotes()\n        .onStart {\n            \/\/ \u5728\u80cc\u666f\u62c9\u53d6\u9060\u7aef\u6700\u65b0\u8cc7\u6599\uff0c\u975c\u9ed8\u5beb\u5165 Room\uff1bRoom \u5beb\u5165\u5b8c\u6210\u5f8c\u6703\u81ea\u52d5\u89f8\u767c Flow \u91cd\u65b0\u767c\u5c04\n            externalScope.launch {\n                remoteDataSource.getAllVotes()\n                    .catch { e -&gt; \/* \u9060\u7aef\u5931\u6557\u6642\u975c\u9ed8\u964d\u7d1a\uff0c\u672c\u5730 Room \u65e2\u6709\u8cc7\u6599\u6beb\u9aee\u7121\u640d *\/ }\n                    .collect { remoteVotes -&gt;\n                        voteDao.syncVotes(remoteVotes)\n                    }\n            }\n        }\n}<\/code><\/pre>\n<p>\u9019\u624d\u662f\u751f\u7522\u7d1a\u7684\u6975\u81f4\u7d14\u7cb9\uff1aUI \u6c38\u9060\u53ea\u8a8d\u8b58 Room\u3002\u4e0d\u8ad6\u662f\u7db2\u8def\u62c9\u53d6\u65b0\u8cc7\u6599\u3001\u4f7f\u7528\u8005\u6309\u4e86\u6536\u85cf\u3001\u9084\u662f\u80cc\u666f\u8cc7\u6599\u540c\u6b65\uff0c\u6240\u6709\u8b8a\u66f4\u4e00\u5f8b\u532f\u5165 Room\uff0c\u518d\u7531 Room \u55ae\u5411\u901a\u77e5 UI\u3002\u72c0\u614b\u6c38\u4e0d\u812b\u7bc0\uff0c\u672c\u5730\u64cd\u4f5c 0 \u6beb\u79d2\u5373\u6642\u56de\u994b\u3002<\/p>\n<hr \/>\n<h2>\u4e09\u3001 \u96d9 Agent \u7d50\u5c0d\uff08Generator-Critic\uff09\u8207\u88ab\u6211\u6293\u5305\u7684\u4e09\u5927\u8cc7\u5b89\u5730\u96f7<\/h2>\n<p>\u5728\u9019\u6b21\u91cd\u69cb\u4e2d\uff0c\u6211\u5011\u63a1\u53d6\u4e86\u96d9 Agent \u5354\u540c\u67b6\u69cb\uff1aGemini \u64d4\u4efb\u624b\u901f\u6975\u5feb\u7684\u5be6\u4f5c\u4e3b\u529b\uff08Generator\uff09\uff0cCodex \u64d4\u4efb\u5c16\u9178\u523b\u8584\u3001\u51b7\u8840\u6311\u523a\u7684\u5be9\u67e5\u54e1\uff08Critic\uff09\u3002<\/p>\n<p>\u5728\u529f\u80fd\u8dd1\u901a\u3001\u6e2c\u8a66\u5168\u7da0\u7684\u8868\u8c61\u4e4b\u4e0b\uff0cCodex \u62ff\u8457\u653e\u5927\u93e1\u9010\u884c\u5be9\u67e5\uff0c\u6293\u51fa\u4e86\u4e09\u500b\u8db3\u4ee5\u8b93\u9019\u6b3e App \u7834\u7522\u5012\u9589\u7684\u81f4\u547d\u5b89\u5168\u8207\u6210\u672c\u5730\u96f7\u3002\u9019\u4e09\u500b\u5730\u96f7\u5728\u4e00\u822c\u6559\u5b78\u6587\u88e1\u5e7e\u4e4e\u7121\u4eba\u63d0\u53ca\uff0c\u4f46\u5728\u96f2\u539f\u751f\u74b0\u5883\u4e0b\uff0c\u6bcf\u4e00\u9846\u90fd\u80fd\u8b93\u4f60\u4ed8\u51fa\u6158\u75db\u4ee3\u50f9\u3002<\/p>\n<h3>\u5730\u96f7\u4e00\uff1aNgramUtil \u6c92\u505a\u622a\u65b7\uff0c\u8d85\u9577\u6587\u5b57\u5f15\u767c Bi-gram \u7d22\u5f15\u7206\u70b8\u8207 DoW \u5e33\u55ae\u653b\u64ca<\/h3>\n<p>Firestore \u6c92\u6709\u63d0\u4f9b\u95dc\u806f\u5f0f\u8cc7\u6599\u5eab\u7684 <code>LIKE<\/code> \u6a21\u7cca\u641c\u5c0b\uff0c\u4e5f\u4e0d\u652f\u63f4\u5168\u6587\u6aa2\u7d22\u3002\u70ba\u4e86\u4e0d\u60f3\u6bcf\u500b\u6708\u82b1\u9322\u5916\u639b Algolia \u6216 Typesense\uff0c\u6211\u5011\u5728\u767c\u8d77\u6295\u7968\u6642\uff0c\u5229\u7528 <code>NgramUtil<\/code> \u81ea\u52d5\u5c07\u6a19\u984c\u5207\u6210\u96d9\u5b57\u5143\uff08Bi-gram\uff09\u5b58\u5165 <code>searchKeywords<\/code> \u9663\u5217\uff0c\u641c\u5c0b\u6642\u7528 <code>whereArrayContains<\/code> \u5be6\u73fe\u96f6\u6210\u672c\u7e41\u4e2d\u641c\u5c0b\u3002<\/p>\n<p>\u9019\u4e3b\u610f\u807d\u8d77\u4f86\u5f88\u68d2\uff0cAI \u5beb\u51fa\u4f86\u7684\u5207\u8a5e\u51fd\u5f0f\u4e5f\u5341\u5206\u4e7e\u6de8\uff1a<\/p>\n<pre><code class=\"language-kotlin\">\/\/ AI \u6700\u521d\u7d66\u51fa\u7684\u7d14\u771f\u7248\u672c\nfun generateBiGrams(text: String): List&lt;String&gt; {\n    val clean = text.trim()\n    return (0 until clean.length - 1).map { clean.substring(it, it + 2).lowercase() }.distinct()\n}<\/code><\/pre>\n<p>\u6e2c\u8a66\u901a\u904e\uff0c\u641c\u5c0b\u529f\u80fd\u7d72\u6ed1\u9806\u66a2\u3002\u4f46 Codex \u4e00\u770b\u7acb\u523b\u4eae\u51fa\u7d05\u724c\uff1a<\/p>\n<p>\u5982\u679c\u4eca\u5929\u6709\u500b\u60e1\u610f\u4f7f\u7528\u8005\uff0c\u5728\u767c\u8d77\u6295\u7968\u7684\u6a19\u984c\u6b04\u4f4d\u88e1\uff0c\u900f\u904e API \u60e1\u610f\u704c\u5165\u4e00\u7bc7 5,000 \u5b57\u7684\u5ee2\u6587\uff0c\u9019\u6bb5\u4ee3\u78bc\u6703\u5207\u51fa\u5c07\u8fd1 5,000 \u500b\u7368\u7acb\u7684\u96d9\u5b57\u5143 token \u585e\u9032 <code>searchKeywords<\/code> \u9663\u5217\uff01\u5728 Firestore \u5e95\u5c64\uff0c\u6bcf\u589e\u52a0\u4e00\u500b\u9663\u5217\u5143\u7d20\uff0c\u8907\u5408\u7d22\u5f15\u5c31\u8981\u591a\u5efa\u7acb\u4e00\u500b\u53cd\u5411\u67e5\u8a62\u7bc0\u9ede\u3002\u66f4\u53ef\u6015\u7684\u662f\uff0cFirestore \u55ae\u4e00\u6587\u6a94\u4e0a\u9650\u662f 1MB\u3002\u9019\u7bc7\u6295\u7968\u4e0d\u53ea\u6703\u76f4\u63a5\u585e\u7206\u6587\u6a94\u4e0a\u9650\u5f15\u767c\u5d29\u6f70\uff0c\u9084\u6703\u5728\u96f2\u7aef\u7d22\u5f15\u5eab\u88e1\u760b\u72c2\u4f54\u7528\u5132\u5b58\u91cf\u3002\u5728\u8cc7\u5b89\u9818\u57df\uff0c\u9019\u53eb<strong>\u9322\u5305\u963b\u65b7\u653b\u64ca\uff08Denial of Wallet, DoW\uff09<\/strong>\u2014\u2014\u653b\u64ca\u8005\u4e0d\u9700\u8981\u62ff\u5230\u4f60\u7684\u4f3a\u670d\u5668\u6b0a\u9650\uff0c\u5149\u9760\u585e\u7206\u4f60\u7684\u7d22\u5f15\uff0c\u5c31\u80fd\u8b93\u4f60\u7684 Google Cloud \u5e33\u55ae\u5728\u6708\u5e95\u66b4\u589e\u5e7e\u5343\u7f8e\u5143\u3002<\/p>\n<p>\u6211\u5011\u5728 <code>feature\/firebase-backend<\/code> \u5206\u652f\u4e0a\uff0c\u7acb\u523b\u88dc\u4e0a\u4e86\u56b4\u683c\u7684\u908a\u754c\u9632\u885b\u8207\u55ae\u5143\u6e2c\u8a66\uff1a<\/p>\n<pre><code class=\"language-diff\">--- a\/funnyvote\/app\/src\/main\/java\/com\/heaton\/funnyvote\/util\/NgramUtil.kt\n+++ b\/funnyvote\/app\/src\/main\/java\/com\/heaton\/funnyvote\/util\/NgramUtil.kt\n@@ -1,12 +1,17 @@\n package com.heaton.funnyvote.util\n \n object NgramUtil {\n+    private const val MAX_INPUT_LENGTH = 60\n+\n     \/**\n      * \u5c07\u8f38\u5165\u5b57\u4e32\u5207\u8a5e\u70ba Bi-gram \u9663\u5217\uff0c\u4f9b Firestore whereArrayContains \u6a21\u7cca\u6bd4\u5c0d\u67e5\u8a62\u3002\n      * \u4f8b\u5982\uff1a\u300c\u5348\u9910\u5403\u4ec0\u9ebc\u300d-&gt; [\"\u5348\u9910\", \"\u9910\u5403\", \"\u5403\u4ec0\", \"\u4ec0\u9ebc\"]\n+     *\n+     * \u8cc7\u5b89\u52a0\u56fa\uff1a\u5f37\u5236\u622a\u65b7\u4e0a\u9650\u70ba 60 \u5b57\u5143\uff0c\u9632\u6b62\u8d85\u9577 Payload \u9020\u6210 Bi-gram \u9663\u5217\u8207\u8a18\u61b6\u9ad4\u7206\u70b8\u3001\n+     * \u4e26\u963b\u65b7\u5c0d Firestore 20,000 \u7d22\u5f15\u4e0a\u9650\u8207 1MB \u55ae\u6587\u6a94\u9ad4\u7a4d\u7684 DoW \u5e33\u55ae\u653b\u64ca\u3002\n      *\/\n     fun generateBiGrams(text: String): List&lt;String&gt; {\n-        val clean = text.trim()\n+        val clean = text.trim().take(MAX_INPUT_LENGTH)\n         if (clean.length &lt; 2) return if (clean.isEmpty()) emptyList() else listOf(clean)\n         val ngrams = mutableSetOf&lt;String&gt;()\n         for (i in 0 until clean.length - 1) {\n             ngrams.add(clean.substring(i, i + 2).lowercase())\n         }\n         return ngrams.toList()\n     }\n }<\/code><\/pre>\n<p>\u7ba1\u4f60\u8f38\u5165\u662f\u4e00\u842c\u5b57\u9084\u662f\u5169\u842c\u5b57\uff0c\u8d85\u904e 60 \u500b\u5b57\u5143\u76f4\u63a5\u5728\u8a18\u61b6\u9ad4\u88e1\u4e00\u5200\u5207\u6389\u3002\u5b88\u4f4f\u9019\u9053\u5e95\u7dda\uff0c\u4f60\u7684\u9322\u5305\u624d\u4fdd\u5f97\u4f4f\u3002<\/p>\n<h3>\u5730\u96f7\u4e8c\uff1aFirestore Rules \u592a\u5929\u771f\uff0c\u4efb\u4f55\u767b\u5165\u8005\u90fd\u80fd\u7ac4\u6539\u5225\u4eba\u7684\u6295\u7968<\/h3>\n<p>\u70ba\u4e86\u89e3\u6c7a\u300c\u6295\u5b8c\u7968\u8981\u628a\u7e3d\u7968\u6578\u52a0\u4e00\u300d\u7684\u9700\u6c42\uff0cAI \u5728 <code>firestore.rules<\/code> \u88e1\u5927\u7b46\u4e00\u63ee\uff0c\u7d66\u51fa\u4e86\u4e00\u884c\u770b\u4f3c\u5408\u60c5\u5408\u7406\u7684\u898f\u5247\uff1a<\/p>\n<pre><code class=\"language-javascript\">\/\/ AI \u7d66\u51fa\u7684\u707d\u96e3\u7d1a\u898f\u5247\nmatch \/polls\/{pollId} {\n    allow read: if true;\n    allow create: if isAuthenticated();\n    allow update: if isAuthenticated(); \/\/ \u53ea\u8981\u767b\u5165\u5c31\u80fd\u66f4\u65b0\uff01\n}<\/code><\/pre>\n<p>\u529f\u80fd\u8dd1\u5f97\u901a\u55ce\uff1f\u5b8c\u5168\u6b63\u5e38\uff0c\u9ede\u64ca\u6295\u7968\uff0c\u7e3d\u7968\u6578\u9806\u5229\u52a0\u4e00\u3002<\/p>\n<p>\u4f46\u9019\u689d\u898f\u5247\u5728\u8cc7\u5b89\u5be9\u67e5\u54e1\u773c\u88e1\uff0c\u7c21\u76f4\u662f\u5728\u91d1\u5eab\u5927\u9580\u8cbc\u4e0a\u300c\u6b61\u8fce\u5149\u81e8\u300d\u3002<\/p>\n<p>\u53ea\u8981\u4efb\u4f55\u4eba\u96a8\u4fbf\u7528\u533f\u540d\u767b\u5165\u63db\u5230\u4e00\u500b UID\uff0c\u63a5\u8457\u62ff Postman \u6216\u64b0\u5beb\u4e00\u6bb5\u7c21\u55ae\u7684 Python \u8173\u672c\uff0c\u4ed6\u53ef\u4ee5\u767c\u9001\u4e00\u500b Update \u8acb\u6c42\uff0c\u76f4\u63a5\u628a\u5168\u7ad9\u6240\u6709\u6295\u7968\u7684\u6a19\u984c\u6539\u6210\u5ee3\u544a\uff0c\u6216\u8005\u628a\u5225\u4eba\u7684 authorId \u7be1\u6539\u6210\u81ea\u5df1\uff0c\u751a\u81f3\u628a\u6240\u6709\u9078\u9805\u7684\u7968\u6578\u6b78\u96f6\u3002<\/p>\n<p>\u6211\u5011\u7acb\u523b\u5728\u5f8c\u7aef\u898f\u5247\u5eab\u5f15\u5165\u4e86\u56b4\u683c\u7684\u6b04\u4f4d\u5dee\u7570\u6bd4\u5c0d\uff08diff\uff09\u8207\u7d50\u69cb\u9a57\u8b49\uff1a<\/p>\n<pre><code class=\"language-diff\">--- a\/firestore.rules\n+++ b\/firestore.rules\n@@ -27,17 +27,39 @@ service cloud.firestore {\n \n     \/\/ 2. \u6295\u7968\u4e3b\u96c6\u5408\n     match \/polls\/{pollId} {\n+      function isValidPollCreate() {\n+        let d = request.resource.data;\n+        return isAuthenticated()\n+          &amp;&amp; d.authorId == request.auth.uid\n+          &amp;&amp; d.title is string &amp;&amp; d.title.size() &gt; 0 &amp;&amp; d.title.size() &lt;= 60\n+          &amp;&amp; (!('description' in d) || d.description == null || (d.description is string &amp;&amp; d.description.size() &lt;= 500))\n+          &amp;&amp; d.optionCount is int &amp;&amp; d.optionCount &gt;= 2 &amp;&amp; d.optionCount &lt;= 10\n+          &amp;&amp; (!('searchKeywords' in d) || (d.searchKeywords is list &amp;&amp; d.searchKeywords.size() &lt;= 60))\n+          &amp;&amp; d.createdAt is int &amp;&amp; d.createdAt &lt;= request.time.toMillis();\n+      }\n+\n+      function isValidPollUpdate() {\n+        return isAuthenticated() &amp;&amp; (\n+          isOwner(resource.data.authorId) ||\n+          request.resource.data.diff(resource.data).affectedKeys().hasOnly(['totalVotes'])\n+        );\n+      }\n+\n       \/\/ \u516c\u958b\u6295\u7968\u5141\u8a31\u6240\u6709\u4eba\u8b80\u53d6\uff1b\u79c1\u4eba\u6295\u7968\u9700\u5df2\u8a8d\u8b49\n       allow read: if resource.data.security == \"00\" || isAuthenticated();\n-      allow create: if isAuthenticated() &amp;&amp; request.resource.data.authorId == request.auth.uid;\n-      allow update: if isAuthenticated();\n+      allow create: if isValidPollCreate();\n+      allow update: if isValidPollUpdate();\n       allow delete: if isAuthenticated() &amp;&amp; resource.data.authorId == request.auth.uid;\n \n       \/\/ \u9078\u9805\u5b50\u96c6\u5408\n       match \/options\/{optionId} {\n         allow read: if true;\n         allow create: if isAuthenticated();\n-        allow update: if isAuthenticated();\n+        allow update: if isAuthenticated() &amp;&amp; (\n+          isOwner(resource.data.creatorId) ||\n+          request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount'])\n+        );\n+        allow delete: if isAuthenticated() &amp;&amp; isOwner(resource.data.creatorId);\n       }<\/code><\/pre>\n<p>\u9019\u6bb5\u898f\u5247\u76f4\u63a5\u628a\u975e\u64c1\u6709\u8005\u7684\u6b0a\u9650\u9396\u6b7b\uff1a\u9664\u975e\u4f60\u662f\u9019\u7bc7\u6295\u7968\u7684\u767c\u8d77\u4eba\uff0c\u5426\u5247\u5728\u6574\u500b\u66f4\u65b0\u4ea4\u6613\u5305\u88e1\uff0c\u88ab\u4fee\u6539\u7684\u6b04\u4f4d\u300c\u6709\u4e14\u50c5\u80fd\u300d\u5305\u542b <code>totalVotes<\/code>\uff01\u53ea\u8981\u4f60\u60f3\u52d5\u6a19\u984c\u3001\u9078\u9805\u5167\u5bb9\u6216\u5efa\u7acb\u6642\u9593\u54ea\u6015\u4e00\u500b byte\uff0cFirestore \u96f2\u7aef\u898f\u5247\u5f15\u64ce\u6703\u76f4\u63a5\u5728\u9580\u53e3\u4ee5 Permission Denied \u5c07\u4f60\u64ca\u6583\u3002<\/p>\n<h3>\u5730\u96f7\u4e09\uff1a\u7f3a\u5c11 Firebase App Check\uff0c\u4efb\u4f55\u4eba\u90fd\u80fd\u62ff\u516c\u958b\u91d1\u9470\u8de8\u904e App \u72c2\u5237\u5f8c\u7aef<\/h3>\n<p>\u53ea\u8981\u5beb\u904e Android \u7684\u4eba\u90fd\u77e5\u9053\uff0c\u6253\u5305\u9032 APK \u88e1\u9762\u7684 <code>google-services.json<\/code> \u6839\u672c\u7b97\u4e0d\u4e0a\u79d8\u5bc6\u3002\u53ea\u8981\u6293\u5230\u4f60\u7684 APK \u89e3\u958b\uff0c\u4efb\u4f55\u8173\u672c\u5c0f\u5b50\u90fd\u80fd\u8f15\u6613\u62ff\u5230\u4f60\u7684 projectId \u8207 apiKey\u3002<\/p>\n<p>\u5982\u679c\u6c92\u6709\u9632\u8b77\uff0c\u653b\u64ca\u8005\u6839\u672c\u4e0d\u9700\u8981\u6253\u958b\u624b\u6a5f\uff0c\u4ed6\u53ea\u8981\u5728\u7d42\u7aef\u6a5f\u88e1\u958b\u4e00\u500b\u5e73\u884c\u8173\u672c\uff0c\u7528 curl \u760b\u72c2\u5411\u4f60\u7684 Firestore REST API \u704c\u5165\u5783\u573e\u6295\u7968\u6216\u5927\u91cf\u904d\u6b77\uff0c\u4f60\u7684\u514d\u8cbb\u8b80\u53d6\u914d\u984d\u5728\u534a\u5c0f\u6642\u5167\u5c31\u6703\u88ab\u6d88\u8017\u6b86\u76e1\u3002<\/p>\n<p>AI \u5b8c\u5168\u6c92\u6709\u610f\u8b58\u5230\u9019\u500b\u79fb\u52d5\u7aef\u7279\u6709\u7684\u653b\u64ca\u9762\u3002Codex \u679c\u65b7\u8981\u6c42\u5728\u61c9\u7528\u7a0b\u5f0f\u5165\u53e3\u63a5\u5165 Google Play Integrity\uff08Firebase App Check\uff09\uff1a<\/p>\n<pre><code class=\"language-diff\">--- a\/funnyvote\/app\/src\/main\/java\/com\/heaton\/funnyvote\/FunnyVoteApplication.kt\n+++ b\/funnyvote\/app\/src\/main\/java\/com\/heaton\/funnyvote\/FunnyVoteApplication.kt\n@@ -10,6 +10,10 @@ import coil.disk.DiskCache\n import coil.memory.MemoryCache\n import coil.request.CachePolicy\n import com.heaton.funnyvote.notification.DailyPollWorker\n+import com.google.firebase.FirebaseApp\n+import com.google.firebase.appcheck.FirebaseAppCheck\n+import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory\n+import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory\n import dagger.hilt.android.HiltAndroidApp\n import java.util.concurrent.TimeUnit\n \n@@ -17,9 +21,24 @@ import java.util.concurrent.TimeUnit\n class FunnyVoteApplication : Application(), ImageLoaderFactory {\n     override fun onCreate() {\n         super.onCreate()\n+        setupFirebaseAppCheck()\n         setupDailyNotificationWorker()\n     }\n \n+    private fun setupFirebaseAppCheck() {\n+        FirebaseApp.initializeApp(this)\n+        val appCheck = FirebaseAppCheck.getInstance()\n+        if (BuildConfig.DEBUG) {\n+            appCheck.installAppCheckProviderFactory(\n+                DebugAppCheckProviderFactory.getInstance()\n+            )\n+        } else {\n+            appCheck.installAppCheckProviderFactory(\n+                PlayIntegrityAppCheckProviderFactory.getInstance()\n+            )\n+        }\n+    }<\/code><\/pre>\n<p>\u9019\u884c\u4ee3\u78bc\u7684\u610f\u7fa9\u5728\u65bc\uff1a\u6bcf\u4e00\u6b21\u767c\u5f80 Firebase \u7684\u7db2\u8def\u8acb\u6c42\uff0c\u90fd\u5fc5\u9808\u593e\u5e36\u4f86\u81ea Android \u7cfb\u7d71\u5e95\u5c64\u5b89\u5168\u6676\u7247\u9812\u767c\u7684\u6b63\u7248\u8a2d\u5099\u6191\u8b49\u3002\u4efb\u4f55\u4f86\u81ea Python \u8173\u672c\u3001\u672a\u7c3d\u540d\u7684\u6a21\u64ec\u5668\u3001\u6216\u662f\u88ab\u4e8c\u5ea6\u6253\u5305\u6ce8\u5165\u60e1\u610f\u4ee3\u78bc\u7684\u76dc\u7248 APK\uff0c\u9023 Firebase \u9598\u9053\u9580\u53e3\u90fd\u9032\u4e0d\u4f86\u3002\u9019\u624d\u53eb\u771f\u6b63\u7684\u79fb\u52d5\u7aef\u7e31\u6df1\u9632\u79a6\u3002<\/p>\n<hr \/>\n<h2>\u56db\u3001 OWASP MASVS v2.0 \u81ea\u52d5\u5316\u8cc7\u5b89\u9632\u8b77\u7db2\uff1a100\/100 (A+) \u7684\u9632\u79a6\u5be6\u8e10<\/h2>\n<p>\u8a31\u591a\u8d70 Vibe Coding \u7684\u958b\u767c\u8005\uff0c\u5f80\u5f80\u6c88\u6d78\u5728\u529f\u80fd\u5feb\u901f\u5806\u758a\u7684\u5feb\u611f\u4e2d\uff0c\u5c0d\u79fb\u52d5\u7aef\u5e95\u5c64\u8cc7\u5b89\u4e00\u7121\u6240\u77e5\u3002\u4f46\u5728 FunnyVote \u5c08\u6848\u88e1\uff0c\u6211\u5011\u5f15\u5165\u4e86\u696d\u754c\u6700\u56b4\u82db\u7684 OWASP MASVS v2.0\uff08Mobile Application Security Verification Standard\uff09\u6a19\u6e96\uff0c\u5728 CI \u6d41\u7a0b\u4e2d\u5efa\u7f6e\u4e86\u81ea\u52d5\u5316\u5b89\u5168\u7a3d\u6838\u3002<\/p>\n<p>\u6211\u5011\u5728\u4ee3\u78bc\u5c64\u843d\u5be6\u4e86\u4e09\u9805\u786c\u6838\u7684\u975c\u614b\u9632\u79a6\uff1a<\/p>\n<ol>\n<li><strong>\u5c01\u6b7b\u7cfb\u7d71\u81ea\u52d5\u5099\u4efd\u901a\u9053<\/strong>\uff1a<br \/>\n\u5728 <code>AndroidManifest.xml<\/code> \u4e2d\u660e\u78ba\u5ba3\u544a\uff1a<br \/>\n<code>&lt;application android:allowBackup=\"false\" android:fullBackupContent=\"false\" ... &gt;<\/code><br \/>\n\u5982\u679c\u4e0d\u52a0\u9019\u884c\uff0c\u653b\u64ca\u8005\u53ea\u8981\u62ff\u5230\u624b\u6a5f\u63d2\u4e0a USB \u7dda\uff0c\u6572\u4e00\u884c <code>adb backup<\/code>\uff0c\u5c31\u80fd\u795e\u4e0d\u77e5\u9b3c\u4e0d\u89ba\u628a\u61c9\u7528\u7a0b\u5f0f\u5167\u79c1\u6709\u7684 Room \u8cc7\u6599\u5eab\u3001SharedPreferences \u4e43\u81f3\u65bc\u767b\u5165 Token \u6574\u5305\u50be\u5370\u51fa\u4f86\u3002\u95dc\u9589\u9019\u500b\u901a\u9053\uff0c\u662f\u8eab\u70ba\u5c08\u696d\u958b\u767c\u8005\u7684\u57fa\u672c\u81ea\u5f8b\u3002<\/li>\n<li><strong>R8 \u7de8\u8b6f\u671f\u9664\u932f\u65e5\u8a8c\u5168\u9762\u62b9\u9664<\/strong>\uff1a<br \/>\n\u5728\u958b\u767c\u968e\u6bb5\uff0c\u6211\u5011\u70ba\u4e86\u9664\u932f\u96a8\u624b\u5beb\u4e0b\u7684 <code>Log.d<\/code>\uff0c\u5f80\u5f80\u5145\u6eff\u4e86\u4f7f\u7528\u8005 UID\u3001\u6295\u7968 ID \u751a\u81f3\u672a\u812b\u654f\u7684\u8cc7\u6599\u7d50\u69cb\u3002\u5728 <code>proguard-rules.pro<\/code> \u88e1\uff0c\u6211\u5011\u52a0\u5165\u4e86\u7de8\u8b6f\u671f\u6700\u4f73\u5316\u6307\u4ee4\uff1a<\/p>\n<pre><code>-assumenosideeffects class android.util.Log {\n    public static boolean isLoggable(java.lang.String, int);\n    public static int v(...);\n    public static int d(...);\n    public static int i(...);\n    public static int w(...);\n    public static int e(...);\n    public static int println(...);\n}<\/code><\/pre>\n<p>\u5728\u6b63\u5f0f\u7522\u51fa Release APK \u6642\uff0cR8 \u975c\u614b\u5206\u6790\u5f15\u64ce\u6703\u5728\u5b57\u7bc0\u78bc\u5c64\u9762\uff0c\u5c07\u6240\u6709\u65e5\u8a8c\u547c\u53eb\u76f4\u63a5\u5f9e\u4e8c\u9032\u5236\u6a94\u6848\u4e2d\u9023\u6839\u62d4\u9664\u3002\u4efb\u6191\u9006\u5411\u5de5\u7a0b\u5e2b\u5982\u4f55\u53cd\u7de8\u8b6f\u6216\u639b\u8f09 Logcat\uff0c\u4f11\u60f3\u5728\u63a7\u5236\u53f0\u770b\u5230\u54ea\u6015\u4e00\u884c\u5167\u90e8\u9664\u932f\u65e5\u8a8c\u3002<\/li>\n<li><strong>\u5168\u57df\u5f37\u5236\u5b89\u5168\u9023\u7dda\u914d\u7f6e<\/strong>\uff1a<br \/>\n\u900f\u904e <code>network_security_config.xml<\/code> \u5c01\u6b7b\u5168\u57df\u6240\u6709\u660e\u6587 HTTP \u8acb\u6c42\u901a\u9053\uff0c\u56b4\u683c\u5f37\u5236\u6240\u6709\u6d41\u91cf\u5fc5\u9808\u8d70 TLS 1.3\u3002<\/li>\n<\/ol>\n<p>\u5728\u6574\u500b\u81ea\u52d5\u5316\u5b89\u5168\u6383\u63cf\u5f15\u64ce\u7684\u6aa2\u6e2c\u4e0b\uff0c\u9019\u5957\u67b6\u69cb\u5728\u67b6\u69cb\u7d14\u5ea6\u3001\u8cc7\u6599\u96b1\u79c1\u3001\u901a\u8a0a\u5b89\u5168\u8207\u91d1\u9470\u9632\u8b77\u56db\u500b\u7dad\u5ea6\u4e0a\uff0c\u4ea4\u51fa\u4e86\u4e00\u5f35 <strong>100\/100 (EXCELLENT A+)<\/strong> \u7684\u5168\u7da0\u6210\u7e3e\u55ae\u3002\u9019\u4e0d\u662f\u70ba\u4e86\u8003\u8a66\uff0c\u800c\u662f\u70ba\u4e86\u5728\u4ea4\u5377\u7684\u90a3\u4e00\u523b\uff0c\u6211\u5011\u80fd\u554f\u5fc3\u7121\u6127\u5730\u8aaa\uff1a\u9019\u662f\u4e00\u5957\u5177\u5099\u751f\u7522\u7b49\u7d1a\u5f37\u5ea6\u7684 App\u3002<\/p>\n<hr \/>\n<h2>\u7d50\u8a9e\uff1aAI \u4e0d\u662f\u795e\uff0c\u662f\u624b\u901f\u6975\u5feb\u4f46\u5b8c\u5168\u4e0d\u61c2\u908a\u754c\u9632\u79a6\u7684\u5be6\u7fd2\u751f<\/h2>\n<p>\u6b77\u6642\u5341\u5e74\u7684 FunnyVote \u91cd\u69cb\u9577\u5f81\uff0c\u5230\u9019\u88e1\u7e3d\u7b97\u756b\u4e0b\u4e86\u6700\u7d2e\u5be6\u7684\u53e5\u9ede\u3002<\/p>\n<p>\u56de\u770b\u9019\u6574\u8d9f Vibe Coding \u5be6\u6230\uff0c\u8eab\u908a\u8a31\u591a\u540c\u884c\u90fd\u5728\u7126\u616e\uff1a\u300cAI \u6572\u4ee3\u78bc\u7684\u901f\u5ea6\u6bd4\u4eba\u985e\u5feb\u4e0a\u4e00\u767e\u500d\uff0c\u5de5\u7a0b\u5e2b\u662f\u4e0d\u662f\u8981\u5931\u696d\u4e86\uff1f\u300d<\/p>\n<p>\u5766\u767d\u8b1b\uff0c\u770b\u8457 AI \u5410\u51fa\u4f86\u7684\u90a3\u4e9b\u7f3a\u4e4f\u908a\u754c\u7d04\u675f\u7684 StateFlow\u3001\u9580\u6236\u5927\u958b\u7684\u8cc7\u6599\u5eab\u898f\u5247\uff0c\u4ee5\u53ca\u5b8c\u5168\u6c92\u9632\u7bc4 DoW \u653b\u64ca\u7684\u5207\u8a5e\u51fd\u5f0f\uff0c\u6211\u7684\u7b54\u6848\u975e\u5e38\u660e\u78ba\uff1a<\/p>\n<p><strong>\u771f\u6b63\u61c2\u67b6\u69cb\u7684\u5de5\u7a0b\u5e2b\uff0c\u73fe\u5728\u6bd4\u4ee5\u5f80\u4efb\u4f55\u6642\u5019\u90fd\u9084\u8981\u91cd\u8981\u3002<\/strong><\/p>\n<p>AI \u78ba\u5be6\u5f88\u5f37\uff0c\u5b83\u5c31\u50cf\u4e00\u500b\u7cbe\u901a\u6240\u6709\u8a9e\u6cd5\u5b57\u5178\u3001\u4e8c\u5341\u56db\u5c0f\u6642\u4e0d\u77e5\u75b2\u5026\u3001\u624b\u901f\u6975\u5feb\u7684\u9802\u5c16\u5be6\u7fd2\u751f\u3002\u53ea\u8981\u4f60\u5429\u5490\u4e00\u8072\uff0c\u5b83\u4e09\u79d2\u9418\u5c31\u80fd\u5e6b\u4f60\u523b\u51fa\u756b\u9762\u3001\u9023\u4e0a\u7db2\u8def\u3001\u628a\u529f\u80fd\u8dd1\u901a\u3002<\/p>\n<p>\u4f46\u9019\u500b\u5be6\u7fd2\u751f\u5b8c\u5168\u6c92\u6709\u5728\u751f\u7522\u74b0\u5883\u88ab\u6454\u6253\u904e\u7684\u75db\u611f\u3002\u5b83\u4e0d\u6703\u66ff\u4f60\u7684 Google Cloud \u5e33\u55ae\u8ca0\u8cac\uff0c\u4e0d\u6703\u66ff\u4f60\u7684\u4f7f\u7528\u8005\u500b\u8cc7\u5916\u6d29\u5750\u7262\uff0c\u66f4\u4e0d\u6703\u5728\u6377\u904b\u65b7\u7db2\u3001\u4f7f\u7528\u8005\u87a2\u5e55\u9583\u9000\u6642\u66ff\u4f60\u64e6\u5c41\u80a1\u3002<\/p>\n<p>\u8eab\u70ba\u4eba\u985e\u5de5\u7a0b\u5e2b\uff0c\u6211\u5011\u5728 AI \u6642\u4ee3\u7684\u7d42\u6975\u50f9\u503c\uff0c\u5f9e\u4f86\u5c31\u4e0d\u662f\u300c\u628a\u9375\u76e4\u6572\u5f97\u591a\u5feb\u300d\u9019\u7a2e\u9ad4\u529b\u52de\u52d5\uff0c\u800c\u662f\uff1a<\/p>\n<ul>\n<li>\u5b9a\u7fa9\u554f\u984c\u672c\u8cea\u7684\u601d\u8003\u529b\uff1b<\/li>\n<li>\u70ba\u7cfb\u7d71\u642d\u5efa\u908a\u754c\u8207\u578b\u5225\u8b77\u6b04\u7684\u67b6\u69cb\u529b\uff1b<\/li>\n<li>\u80fd\u5920\u5728\u4e00\u7247\u5168\u7da0\u7684\u55ae\u5143\u6e2c\u8a66\u4e2d\uff0c\u654f\u92b3\u6293\u51fa\u8cc7\u5b89\u6f0f\u6d1e\u8207\u7834\u7522\u96b1\u60a3\u7684\u5be9\u67e5\u529b\u3002<\/li>\n<\/ul>\n<p>\u300c\u6020\u60f0\u662f\u958b\u767c\u7684\u6e90\u982d\u300d\uff0c\u9019\u53e5\u8a71\u6211\u8aaa\u4e86\u6574\u6574\u5341\u5e74\u3002\u4f46\u7d93\u904e\u9019\u5341\u5e74\u7684\u6298\u9a30\u8207\u6c89\u6fb1\uff0c\u6211\u7d42\u65bc\u660e\u767d\uff1a\u8981\u5077\u61f6\uff0c\u4f60\u5f97\u5148\u6709\u8db3\u5920\u7684\u672c\u4e8b\u628a\u8b77\u6b04\u7bc9\u5f97\u6bd4\u5c71\u9084\u9ad8\u3002\u53ea\u6709\u628a\u5e95\u7dda\u515c\u5f97\u7a69\u7a69\u7576\u7576\uff0c\u672a\u4f86\u7684\u4f60\uff0c\u624d\u80fd\u771f\u6b63\u512a\u96c5\u3001\u5fc3\u5b89\u7406\u5f97\u5730\u6020\u60f0\u4e0b\u53bb\u3002<\/p>\n<p>\u5c0f\u5f1f\u6211\u771f\u7684\u61f6\u5f97\u8cbc\u843d\u843d\u9577\u7684 CODE\uff0c\u5404\u4f4d\u60f3\u7ffb\u7ffb\u770b\u5b8c\u6574\u7684 MASVS \u6383\u63cf\u5831\u544a\u3001Firestore \u5b89\u5168\u898f\u5247\u8207\u9019\u5341\u5e74\u9593\u6f14\u9032\u7684\u6240\u6709\u9ed1\u6b77\u53f2\uff0c\u901a\u901a\u90fd\u5728 GitHub \u4e0a\u4e86\uff1a<code>boochlin06\/FunnyVote<\/code>\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u5728\u524d\u9762\u516d\u7bc7\u6f2b\u9577\u7684\u6f14\u9032\u96dc\u8a18\u88e1\uff0c\u6211\u5011\u4e00\u8def\u5f9e 2016 \u5e74\u624b\u6572 ButterKnife \u7684\u820a\u77f3\u5668\u6642\u4ee3\uff0c\u804a\u5230 2018 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1314,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[86,169],"tags":[6,172,179,128],"_links":{"self":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts\/1315"}],"collection":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1315"}],"version-history":[{"count":1,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts\/1315\/revisions"}],"predecessor-version":[{"id":1317,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/posts\/1315\/revisions\/1317"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=\/wp\/v2\/media\/1314"}],"wp:attachment":[{"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1315"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1315"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/boochlin.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1315"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}