{"id":11639,"date":"2025-12-16T15:47:54","date_gmt":"2025-12-16T04:47:54","guid":{"rendered":"https:\/\/geoffreychen.com\/?p=11639"},"modified":"2025-12-17T00:27:01","modified_gmt":"2025-12-16T13:27:01","slug":"%e4%bb%8e%e4%b8%80%e4%b8%aa%e7%99%bb%e5%bd%95%e8%ae%a1%e6%95%b0%e9%97%ae%e9%a2%98%e8%b0%88%e5%88%86%e5%b1%82%e6%9e%b6%e6%9e%84%e7%9a%84%e8%ae%be%e8%ae%a1%e5%8e%9f%e5%88%99%e4%b8%8e","status":"publish","type":"post","link":"https:\/\/geoffreychen.com\/zh\/%e4%bb%8e%e4%b8%80%e4%b8%aa%e7%99%bb%e5%bd%95%e8%ae%a1%e6%95%b0%e9%97%ae%e9%a2%98%e8%b0%88%e5%88%86%e5%b1%82%e6%9e%b6%e6%9e%84%e7%9a%84%e8%ae%be%e8%ae%a1%e5%8e%9f%e5%88%99%e4%b8%8e\/","title":{"rendered":"\u4ece\u4e00\u4e2a\u201c\u767b\u5f55\u8ba1\u6570\u201d\u95ee\u9898\u8c08\u5206\u5c42\u67b6\u6784\u7684\u8bbe\u8ba1\u539f\u5219\u4e0e\u5b9e\u8df5\u6280\u5de7 From a \u201cLogin Counting\u201d Question to Principles and Practical Techniques of Layered Architecture Design"},"content":{"rendered":"<p class=\"wp-block-paragraph\">\u4ece\u4e00\u4e2a\u201c\u767b\u5f55\u8ba1\u6570\u201d\u95ee\u9898\u8c08\u5206\u5c42\u67b6\u6784\u7684\u8bbe\u8ba1\u539f\u5219\u4e0e\u5b9e\u8df5\u6280\u5de7<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By Geoffrey Chen<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5728\u8fdb\u884c\u5206\u5c42\u67b6\u6784\u7684\u8bbe\u8ba1\u4e0e\u5f00\u53d1\u65f6\uff0c\u6211\u4eec\u7ecf\u5e38\u4f1a\u9047\u5230\u4e00\u4e9b\u770b\u4f3c\u7ec6\u5fae\u3001\u5374\u6781\u5bb9\u6613\u5f15\u53d1\u6df7\u6dc6\u7684\u95ee\u9898\u3002\u8fd9\u4e9b\u95ee\u9898\u5f80\u5f80\u8868\u9762\u4e0a\u662f\u201c\u4ee3\u7801\u5e94\u8be5\u653e\u5728\u54ea\u4e00\u5c42\u201d\uff0c\u4f46\u6df1\u5165\u601d\u8003\u540e\u4f1a\u53d1\u73b0\uff0c\u5b83\u4eec\u5b9e\u9645\u89e6\u53ca\u7684\u662f\u67b6\u6784\u5206\u5c42\u7684\u8bed\u4e49\u8fb9\u754c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ee5\u6211\u4eec\u5f00\u53d1\u7684 Smallsoft Identity\uff08SI\uff09 \u4e3a\u4f8b\uff0c\u5c31\u9047\u5230\u4e86\u8fd9\u6837\u4e00\u4e2a\u770b\u4f3c\u7b80\u5355\u3001\u5374\u975e\u5e38\u6709\u542f\u53d1\u6027\u7684\u5177\u4f53\u95ee\u9898\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528\u6237\u767b\u5f55\u6210\u529f\u4e4b\u540e\uff0c\u767b\u5f55\u6b21\u6570\uff08Login Usage\uff09\u5e94\u8be5\u5728\u54ea\u91cc\u7edf\u8ba1\uff1f\u662f\u5728 Api \u5c42\uff0c\u8fd8\u662f\u5728 Application \u5c42\uff1f\u4e4d\u770b\u4e4b\u4e0b\uff0c\u8fd9\u4f3c\u4e4e\u53ea\u662f\u4e00\u4e2a\u5b9e\u73b0\u7ec6\u8282\uff1b\u4f46\u5728\u6df1\u5165\u8ba8\u8bba\u4e4b\u540e\uff0c\u5b83\u5b9e\u9645\u4e0a\u63ed\u793a\u4e86\u5206\u5c42\u67b6\u6784\u4e2d\u4e00\u4e2a\u975e\u5e38\u6838\u5fc3\u3001\u5374\u7ecf\u5e38\u88ab\u5ffd\u7565\u7684\u8bbe\u8ba1\u539f\u5219\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8868\u8c61\u4e0a\u7684\u201c\u5408\u7406\u201d\uff1a\u5728 Api \u5c42\u7edf\u8ba1\u767b\u5f55<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ece\u4ee3\u7801\u7ed3\u6784\u4e0a\u770b\uff0c\u628a\u767b\u5f55\u7edf\u8ba1\u653e\u5728 Api \u5c42\u662f\u4e00\u4e2a\u975e\u5e38\u81ea\u7136\u7684\u60f3\u6cd5\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">var result = await _login.ExecuteAsync(request);<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">if (result.Success)<br>{<br>\/\/ \u5728\u8fd9\u91cc\u7edf\u8ba1\u4e00\u6b21\u767b\u5f55\uff1f<br>}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Api \u5c42\u63a5\u6536 HTTP \u8bf7\u6c42\uff0c\u8c03\u7528 Application \u5c42\u7684 LoginUserCommand\uff0c\u5e76\u5f97\u5230\u4e86\u4e00\u4e2a Success = true \u7684\u8fd4\u56de\u7ed3\u679c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u76f4\u89c9\u5f88\u5bb9\u6613\u5f97\u51fa\u8fd9\u6837\u7684\u7ed3\u8bba\uff1a\u201c\u767b\u5f55\u6210\u529f\u4e86\uff0c\u90a3\u5c31\u5728\u8fd9\u91cc\u7edf\u8ba1\u4e00\u6b21\u767b\u5f55\u5427\u3002\u201d \u4e8b\u5b9e\u4e0a\uff0c\u8fd9\u79cd\u505a\u6cd5\u5728\u4e0d\u5c11\u7cfb\u7edf\u4e2d\u90fd\u5f88\u5e38\u89c1\u3002\u4f46\u95ee\u9898\u5728\u4e8e\u2014\u2014\u8fd9\u79cd\u505a\u6cd5\u5728\u67b6\u6784\u8bed\u4e49\u4e0a\u662f\u9519\u8bef\u7684\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5173\u952e\u5206\u6b67\u70b9\uff1aApi \u77e5\u9053\u201c\u6210\u529f\u201d\uff0c\u4f46\u4e0d\u77e5\u9053\u201c\u53d1\u751f\u4e86\u4ec0\u4e48\u201d Api \u5c42\u771f\u6b63\u77e5\u9053\u7684\u4e8b\u5b9e\u53ea\u6709\u4e00\u4e2a\uff1a\u201c\u67d0\u4e2a\u8bf7\u6c42\u6210\u529f\u8fd4\u56de\u4e86\u3002\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f46 Api \u5c42\u5e76\u4e0d\u77e5\u9053\u3001\u4e5f\u4e0d\u5e94\u8be5\u53bb\u5224\u65ad\u4ee5\u4e0b\u8fd9\u4e9b\u5173\u952e\u7684\u4e1a\u52a1\u8bed\u4e49\u4e8b\u5b9e\uff1a<br>\u2022 \u662f\u5426\u521b\u5efa\u4e86\u4e00\u4e2a\u65b0\u7684\u8ba4\u8bc1\u4f1a\u8bdd<br>\u2022 \u662f\u5426\u590d\u7528\u4e86\u5df2\u6709\u7684 Session<br>\u2022 \u662f\u5426\u5c5e\u4e8e\u9759\u9ed8\u8ba4\u8bc1\u6216\u81ea\u52a8\u767b\u5f55<br>\u2022 \u5c06\u6765 LoginUserCommand \u7684\u5185\u90e8\u5b9e\u73b0\u662f\u5426\u4f1a\u53d1\u751f\u53d8\u5316<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u6362\u53e5\u8bdd\u8bf4\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Api \u5c42\u770b\u5230\u7684\u662f\u201c\u63a7\u5236\u6d41\u6210\u529f\u201d\uff0c\u800c\u4e0d\u662f\u201c\u4e1a\u52a1\u72b6\u6001\u53d1\u751f\u4e86\u4ec0\u4e48\u53d8\u5316\u201d\u3002\u800c\u201c\u767b\u5f55\u201d\u672c\u8eab\uff0c\u6070\u6070\u4e0d\u662f\u4e00\u4e2a\u63a7\u5236\u6d41\u6982\u5ff5\uff0c\u800c\u662f\u4e00\u4e2a\u4e1a\u52a1\u72b6\u6001\u53d8\u5316\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u6b63\u7684\u201c\u767b\u5f55\u201d\u53d1\u751f\u5728\u54ea\u91cc\uff1f\u5728 SI \u7684\u5b9e\u73b0\u4e2d\uff0c\u201c\u767b\u5f55\u201d\u771f\u6b63\u53d1\u751f\u5728\u8fd9\u4e00\u523b\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">await _sessions.CreateSessionAsync(new Session { \u2026 });<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e00\u884c\u4ee3\u7801\u6240\u8868\u8fbe\u7684\u4e1a\u52a1\u8bed\u4e49\u975e\u5e38\u6e05\u6670\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201c\u4e00\u4e2a\u65b0\u7684\u3001\u5df2\u8ba4\u8bc1\u7684\u7528\u6237\u4f1a\u8bdd\u88ab\u521b\u5efa\u4e86\u3002\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u4e00\u4e2a\u660e\u786e\u7684\u3001\u4e0d\u53ef\u6b67\u4e49\u7684\u3001\u4e0d\u53ef\u56de\u9000\u7684\u4e1a\u52a1\u4e8b\u5b9e\u3002\u800c\u8fd9\u4e00\u4e8b\u5b9e\u53ea\u5b58\u5728\u4e8e Application \u5c42\u7684\u7528\u4f8b\u903b\u8f91\u4e2d\uff0c\u800c\u4e0d\u662f Api \u5c42\u3002\u56e0\u6b64\uff0c\u4e00\u4e2a\u5173\u952e\u7ed3\u8bba\u81ea\u7136\u6d6e\u73b0\u51fa\u6765\uff1aLogin \u4e0d\u662f Api \u4e8b\u4ef6\uff0c\u800c\u662f Application \u4e8b\u4ef6\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u53ef\u4ee5\u53cd\u590d\u4f7f\u7528\u7684\u5206\u5c42\u539f\u5219<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7531\u6b64\uff0c\u6211\u4eec\u53ef\u4ee5\u603b\u7ed3\u51fa\u4e00\u4e2a\u975e\u5e38\u5b9e\u7528\u3001\u4e14\u53ef\u4ee5\u5728\u5f88\u591a\u7cfb\u7edf\u4e2d\u53cd\u590d\u5e94\u7528\u7684\u5206\u5c42\u539f\u5219\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u4e00\u4e2a\u884c\u4e3a\u4ee3\u8868\u7684\u662f\u201c\u4e1a\u52a1\u4e8b\u5b9e\u5df2\u7ecf\u6210\u7acb\u201d\uff0c\u90a3\u4e48\u5b83\u5e94\u8be5\u5728 Application \u5c42\u88ab\u786e\u8ba4\u548c\u8bb0\u5f55\uff1bApi \u5c42\u53ea\u80fd\u8d1f\u8d23\u8bf7\u6c42\u8f6c\u53d1\u4e0e\u7ed3\u679c\u8fd4\u56de\uff0c\u800c\u4e0d\u62e5\u6709\u4e1a\u52a1\u8bed\u4e49\u7684\u88c1\u91cf\u6743\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e2a\u539f\u5219\u7684\u53cd\u9762\u540c\u6837\u6e05\u6670\uff1a<br>\u2022 Api \u5c42\uff1a\u9002\u5408\u5904\u7406\u534f\u8bae\u7ea7\u3001\u4f20\u8f93\u7ea7\u3001\u8868\u73b0\u7ea7\u4e8b\u4ef6<br>\u2022 Application \u5c42\uff1a\u9002\u5408\u5904\u7406\u4e1a\u52a1\u5b8c\u6210\u3001\u72b6\u6001\u8f6c\u6362\u3001\u4e8b\u5b9e\u6210\u7acb<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e3a\u4ec0\u4e48 Access Token \u7684\u8ba1\u6570\u53ef\u4ee5\u653e\u5728 Api \u5c42\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5728\u540c\u4e00\u4e2a\u7cfb\u7edf\u4e2d\uff0c\u6211\u4eec\u786e\u5b9e\u9009\u62e9\u4e86\u5728 OAuth Controller\uff08Api \u5c42\uff09\u7edf\u8ba1 Access Token \u7684\u53d1\u653e\u6b21\u6570\u3002\u8fd9\u5e76\u4e0d\u77db\u76fe\u3002\u539f\u56e0\u5728\u4e8e\uff1a<br>\u2022 Access Token \u662f OAuth \u534f\u8bae\u7684\u76f4\u63a5\u4ea7\u7269<br>\u2022 Token\u201c\u88ab\u53d1\u653e\u201d\u8fd9\u4e00\u4e8b\u5b9e\uff0c\u53ea\u5728 OAuth Controller \u4e2d\u6210\u7acb<br>\u2022 \u5f53\u524d\u7cfb\u7edf\u4e2d\u5e76\u4e0d\u5b58\u5728\u4e00\u4e2a\u534f\u8bae\u65e0\u5173\u7684 IssueAccessToken \u4e1a\u52a1\u7528\u4f8b<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u6b64\uff1a<br>\u2022 Login = \u4e1a\u52a1\u4e8b\u5b9e \u2192 Application \u5c42<br>\u2022 Access Token Issued = \u534f\u8bae\u4e8b\u5b9e \u2192 Api \u5c42<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4f53\u73b0\u7684\u662f\u4e00\u79cd\u8bed\u4e49\u4e0a\u7684\u4e00\u81f4\u6027\uff0c\u800c\u4e0d\u662f\u5f62\u5f0f\u4e0a\u7684\u5bf9\u79f0\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e00\u539f\u5219\u5e26\u6765\u7684\u957f\u671f\u6536\u76ca<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5c06 Login \u8ba1\u6570\u653e\u5728 Application \u5c42\uff0c\u4f1a\u5e26\u6765\u4e00\u7cfb\u5217\u975e\u5e38\u201c\u5de5\u7a0b\u5316\u201d\u7684\u957f\u671f\u6536\u76ca\uff1a<br>\u2022 \u6240\u6709\u767b\u5f55\u65b9\u5f0f\uff08IdentityWeb\u3001OAuth\u3001\u672a\u6765\u7684\u8bbe\u5907\u6d41\u7b49\uff09\u90fd\u4f1a\u88ab\u81ea\u52a8\u8986\u76d6<br>\u2022 \u4e0d\u4f1a\u56e0 Api \u8def\u5f84\u8c03\u6574\u800c\u6f0f\u7b97\u6216\u91cd\u590d\u8ba1\u7b97<br>\u2022 Billing\u3001MAU\u3001\u5ba1\u8ba1\u7b49\u7edf\u8ba1\u8bed\u4e49\u66f4\u52a0\u7a33\u5b9a\u3001\u53ef\u89e3\u91ca<br>\u2022 Application \u5c42\u81ea\u7136\u6210\u4e3a\u4e1a\u52a1\u4e8b\u5b9e\u7684\u552f\u4e00\u771f\u6e90\uff08Single Source of Truth\uff09<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e2a\u4f8b\u5b50\u8868\u9762\u4e0a\u662f\u5728\u8ba8\u8bba\uff1a\u201c\u767b\u5f55\u8ba1\u6570\u903b\u8f91\u5e94\u8be5\u653e\u5728\u54ea\u91cc\uff1f\u201d\u4f46\u672c\u8d28\u4e0a\uff0c\u5b83\u56de\u7b54\u7684\u662f\u4e00\u4e2a\u66f4\u91cd\u8981\u7684\u95ee\u9898\uff1a\u201c\u8c01\u6709\u6743\u5b9a\u4e49\u4e00\u4e2a\u4e1a\u52a1\u4e8b\u5b9e\u5df2\u7ecf\u53d1\u751f\uff1f\u201d\u5728\u4e00\u4e2a\u5065\u5eb7\u3001\u53ef\u6f14\u8fdb\u7684\u5206\u5c42\u67b6\u6784\u4e2d\uff0c\u7b54\u6848\u59cb\u7ec8\u662f\uff1aApplication \u5c42\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Api \u5c42\u8d1f\u8d23\u628a\u8bf7\u6c42\u9001\u5230\u6b63\u786e\u7684\u5730\u65b9\uff0c\u800c Application \u5c42\u8d1f\u8d23\u51b3\u5b9a\uff1a\u4e16\u754c\u53d1\u751f\u4e86\u4ec0\u4e48\u53d8\u5316\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u65e6\u786e\u7acb\u4e86\u8fd9\u4e00\u539f\u5219\uff0c\u8bb8\u591a\u539f\u672c\u6a21\u7cca\u3001\u7ea0\u7ed3\u7684\u67b6\u6784\u9009\u62e9\uff0c\u90fd\u4f1a\u81ea\u7136\u53d8\u5f97\u6e05\u6670\u800c\u575a\u5b9a\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From a \u201cLogin Counting\u201d Question to Principles and Practical Techniques of Layered Architecture Design<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Geoffrey Chen<br>December 16, 2025<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When designing and developing systems with a layered architecture, we often encounter issues that appear subtle yet are highly prone to confusion. On the surface, these issues usually take the form of questions such as \u201cWhich layer should this code belong to?\u201d But upon deeper reflection, we often find that they actually touch on the semantic boundaries between architectural layers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">During the development of Smallsoft Identity (SI), we encountered such a seemingly simple but highly instructive question:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After a user successfully logs in, where should the login count (Login Usage) be recorded?<br>In the Api layer, or in the Application layer?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At first glance, this looks like a mere implementation detail.<br>However, after careful discussion, it turns out to reveal a core design principle of layered architecture that is frequently overlooked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Superficially \u201cReasonable\u201d Choice: Counting Logins in the Api Layer<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From a code-structure perspective, counting logins in the Api layer feels like a very natural idea:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">var result = await _login.ExecuteAsync(request);<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">if (result.Success)<br>{<br>\/\/ Count one login here?<br>}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Api layer receives an HTTP request, invokes the LoginUserCommand in the Application layer, and receives a result with Success = true.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is easy to arrive at the intuitive conclusion:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cThe login succeeded, so let\u2019s count the login here.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In fact, this approach is quite common in many systems.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But the problem is this \u2014 architecturally, this approach is semantically incorrect.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Key Point of Divergence: Api Knows \u201cSuccess,\u201d but Not \u201cWhat Happened\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The only fact the Api layer truly knows is:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cA request returned successfully.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, the Api layer neither knows nor should attempt to judge the following critical business-semantic facts:<br>\u2022 Whether a new authenticated session was created<br>\u2022 Whether an existing session was reused<br>\u2022 Whether the login was silent or automatic<br>\u2022 Whether the internal implementation of LoginUserCommand might change in the future<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In other words:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Api layer observes \u201ccontrol-flow success,\u201d<br>not \u201cbusiness-state transition.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And login itself is not a control-flow concept\u2014it is a business state change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Where Does a \u201cLogin\u201d Actually Occur?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the SI implementation, a login truly occurs at this moment:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">await _sessions.CreateSessionAsync(new Session { \u2026 });<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The business meaning expressed by this line is very clear:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cA new, authenticated user session has been created.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is a definite, unambiguous, and irreversible business fact.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And this fact exists only within the use-case logic of the Application layer, not in the Api layer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Therefore, a key conclusion naturally emerges:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Login is not an Api event; it is an Application event.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A Reusable Layering Principle<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From this, we can distill a highly practical layering principle that can be applied repeatedly across many systems:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If an action represents the establishment of a business fact,<br>it should be confirmed and recorded in the Application layer;<br>the Api layer should only be responsible for request forwarding and result delivery,<br>and should not possess authority over business semantics.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The inverse of this principle is equally clear:<br>\u2022 Api layer: suitable for protocol-level, transport-level, and presentation-level events<br>\u2022 Application layer: suitable for business completion, state transitions, and facts becoming true<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Why Access Token Counting Can Reside in the Api Layer<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the same system, we do choose to count Access Token issuance in the OAuth Controller (Api layer).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is not a contradiction.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The reasons are:<br>\u2022 An Access Token is a direct product of the OAuth protocol<br>\u2022 The fact that a token is \u201cissued\u201d only becomes true within the OAuth Controller<br>\u2022 There is currently no protocol-independent IssueAccessToken business use case<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Therefore:<br>\u2022 Login = business fact \u2192 Application layer<br>\u2022 Access Token Issued = protocol fact \u2192 Api layer<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This reflects semantic consistency, rather than formal symmetry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Long-Term Benefits of This Principle<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Placing login counting in the Application layer brings a series of highly practical, engineering-oriented long-term benefits:<br>\u2022 All login methods (IdentityWeb, OAuth, future device flows, etc.) are automatically covered<br>\u2022 No missed counts or double counting caused by Api route changes<br>\u2022 Billing, MAU, and audit semantics become more stable and explainable<br>\u2022 The Application layer naturally becomes the Single Source of Truth for business facts<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On the surface, this example discusses the question:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cWhere should login counting logic be placed?\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At a deeper level, it answers a far more important question:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cWho has the authority to define that a business fact has occurred?\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In a healthy, evolvable layered architecture, the answer is always:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Application layer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Api layer is responsible for delivering requests to the correct place,<br>while the Application layer is responsible for deciding what has actually changed in the world.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once this principle is firmly established, many architectural decisions that once felt ambiguous or conflicted naturally become clear and decisive.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<p class=\"wp-block-paragraph\">Contact: Geoffrey Chen&nbsp;<a href=\"mailto:me@geoffreychen.com\">me@geoffreychen.com<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Verified GPG Public Key:&nbsp;<a href=\"https:\/\/keys.openpgp.org\/search?q=me@geoffreychen.com\">https:\/\/keys.openpgp.org\/search?q=me@geoffreychen.com<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>","protected":false},"excerpt":{"rendered":"<p>\u5728\u8fdb\u884c\u5206\u5c42\u67b6\u6784\u7684\u8bbe\u8ba1\u4e0e\u5f00\u53d1\u65f6\uff0c\u6211\u4eec\u7ecf\u5e38\u4f1a\u9047\u5230\u4e00\u4e9b\u770b\u4f3c\u7ec6\u5fae\u3001\u5374\u6781\u5bb9\u6613\u5f15\u53d1\u6df7\u6dc6\u7684\u95ee\u9898\u3002\u8fd9\u4e9b\u95ee\u9898\u5f80\u5f80\u8868\u9762\u4e0a\u662f\u201c\u4ee3\u7801\u5e94\u8be5\u653e\u5728\u54ea\u4e00\u5c42\u201d\uff0c\u4f46\u6df1\u5165\u601d\u8003\u540e\u4f1a\u53d1\u73b0\uff0c\u5b83\u4eec\u5b9e\u9645\u89e6\u53ca\u7684\u662f\u67b6\u6784\u5206\u5c42\u7684\u8bed\u4e49\u8fb9\u754c\u3002<\/p>","protected":false},"author":17897162,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"image","meta":{"_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":true,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_geoff_meta_description_en":"","_geoff_meta_description_zh":"","_wpcom_ai_launchpad_first_post":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[784633319],"tags":[],"class_list":["post-11639","post","type-post","status-publish","format-image","hentry","category-ai-frontiers","post_format-post-format-image",""],"jetpack_publicize_connections":[],"jetpack_likes_enabled":false,"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p1ar9H-31J","jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/posts\/11639","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/users\/17897162"}],"replies":[{"embeddable":true,"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/comments?post=11639"}],"version-history":[{"count":6,"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/posts\/11639\/revisions"}],"predecessor-version":[{"id":11646,"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/posts\/11639\/revisions\/11646"}],"wp:attachment":[{"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/media?parent=11639"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/categories?post=11639"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/geoffreychen.com\/zh\/wp-json\/wp\/v2\/tags?post=11639"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}