:從架構(gòu)設(shè)計到庫存并發(fā)控制實戰(zhàn))
1. 項目背景與需求定位1.1 這個項目到底解決什么問題做這個零售與倉儲管理系統(tǒng)之前我最直觀的感受是很多小型零售企業(yè)還在用Excel管庫存甚至靠手寫賬本。倉庫里進(jìn)了什么貨、賣了什么、還剩多少全依賴人工登記。一旦商品種類超過幾百個或者門店和倉庫不在同一個地方這種模式的混亂程度會直線上升。庫存對不上賬、補貨不及時、月底盤點費時費力這些都是常態(tài)。所以我當(dāng)時做這個項目的核心目標(biāo)很明確用一套系統(tǒng)打通零售前端和倉儲后端讓進(jìn)貨、入庫、出庫、銷售、盤點這幾條核心鏈路在一個系統(tǒng)里運轉(zhuǎn)起來。Spring Boot作為項目底座不僅僅是圖它開發(fā)效率高更重要的是它的生態(tài)足夠成熟后續(xù)擴展權(quán)限管理、對接支付、生成報表等能力都很順手。1.2 適合誰參考這套設(shè)計如果你是剛學(xué)完Java基礎(chǔ)想找一個完整的項目練手這個系統(tǒng)的復(fù)雜度恰到好處。它不像那種純CRUD的圖書管理系統(tǒng)那么簡單也不像分布式電商那么復(fù)雜模塊劃分、權(quán)限控制、庫存邏輯都有足夠的深度可以挖掘。如果你是在小公司做內(nèi)部管理系統(tǒng)這套方案的可復(fù)制度很高。零售加倉儲的組合非常常見核心模型拎出來改改可以直接遷移到其他行業(yè)的管理系統(tǒng)上。我做這套系統(tǒng)時有一個很深的體會項目的價值不在于用了多新的技術(shù)而在于業(yè)務(wù)邏輯是否梳理得清晰數(shù)據(jù)是否準(zhǔn)確可靠。這一點這個項目體現(xiàn)得很充分。2. 整體架構(gòu)與技術(shù)選型思路2.1 為什么選Spring Boot而不是其他框架技術(shù)選型這一步我先說說自己的考量。之前用SSHStrutsSpringHibernate做過類似項目配置文件多到讓人崩潰每加一個類都要去XML里注冊。Spring Boot最大的優(yōu)勢就是自動配置和約定優(yōu)于配置省掉了大量繁瑣的裝配工作。開發(fā)階段用內(nèi)嵌的Tomcat跑起來打包成可執(zhí)行Jar丟到服務(wù)器上就能運行部署成本非常低。Java這個語言選得也比較穩(wěn)。零售倉儲系統(tǒng)涉及資金和貨物穩(wěn)定性和事務(wù)能力是第一位的。Java在這方面的積累很深JDBC、JPA、MyBatis這些數(shù)據(jù)訪問層方案都非常成熟遇到復(fù)雜查詢或者事務(wù)問題社區(qū)里能搜到大量現(xiàn)成的解決方案。2.2 核心依賴與分層結(jié)構(gòu)我用的技術(shù)棧是Spring Boot 2.x MyBatis Plus MySQL Redis。Spring Boot負(fù)責(zé)整體框架和請求分發(fā)MyBatis Plus負(fù)責(zé)數(shù)據(jù)訪問MySQL存業(yè)務(wù)數(shù)據(jù)Redis存登錄會話和緩存熱數(shù)據(jù)。分層結(jié)構(gòu)上我保持了經(jīng)典的四層架構(gòu)Controller層接收前端請求Service層寫核心業(yè)務(wù)邏輯Mapper層做數(shù)據(jù)庫交互實體類映射表結(jié)構(gòu)。這里要強調(diào)一點很多初學(xué)者容易把業(yè)務(wù)邏輯寫在Controller里圖省事。但一旦業(yè)務(wù)復(fù)雜起來Controller膨脹得飛快后期幾乎沒法維護。我的習(xí)慣是Controller只做參數(shù)接收和結(jié)果包裝所有判斷、計算、異常處理都下沉到Service層。2.3 為什么用RBAC模型做權(quán)限管理權(quán)限管理我采用了基于角色的訪問控制模型RBAC就是給用戶分配角色給角色分配權(quán)限。超級管理員、倉庫管理員、銷售員、店長這幾種角色覆蓋了大部分業(yè)務(wù)場景。每個用戶登錄后系統(tǒng)根據(jù)其角色返回可見菜單和可操作的接口。這個設(shè)計看起來簡單實際操作時有兩個很關(guān)鍵的點。第一個是權(quán)限粒度我控制到了按鈕級別哪些角色能看到“刪除商品”按鈕哪些角色只能看“編輯”都在前端根據(jù)權(quán)限標(biāo)識進(jìn)行控制。第二個是后端接口必須再做一次校驗不能光靠前端隱藏按鈕。我曾見過一些項目前端把按鈕藏得很嚴(yán)實結(jié)果有人直接調(diào)接口就繞過了限制這個問題務(wù)必避免。3. 數(shù)據(jù)庫設(shè)計與核心功能模塊切分3.1 數(shù)據(jù)表規(guī)劃的核心邏輯數(shù)據(jù)庫設(shè)計是整個系統(tǒng)最重要的一環(huán)。我總結(jié)一個原則寧可多拆幾張表不要把所有字段塞進(jìn)一張大表。合理的表結(jié)構(gòu)能讓后續(xù)的統(tǒng)計和擴展變得很順暢。我設(shè)計的主要數(shù)據(jù)表包括用戶表、角色表、權(quán)限表、商品分類表、商品信息表、供應(yīng)商表、倉庫表、庫存表、入庫單表、出庫單表、銷售訂單表、盤點記錄表。每個表的主鍵用自增ID所有金額字段統(tǒng)一用Decimal類型避免浮點數(shù)精度問題。以商品表為例字段不止包括商品名稱、條碼、規(guī)格、單位這些基礎(chǔ)信息還專門加了預(yù)警閾值字段。這個預(yù)警閾值是庫存管理的關(guān)鍵當(dāng)庫存數(shù)量低于這個值時系統(tǒng)自動在首頁生成補貨提醒。有些系統(tǒng)把預(yù)警寫死在代碼里不同商品用一個閾值這其實不太符合實際情況。比如礦泉水可能剩100瓶還很充裕但一臺昂貴的設(shè)備剩1臺就得補貨。閾值放在商品表里按商品單獨配置這個設(shè)計我建議直接抄走。3.2 庫存字段到底放哪張表庫存的設(shè)計上我踩過一個小坑。最開始我把庫存數(shù)量直接放在商品表里查詢是方便了但每次出入庫都要更新商品表而且沒法追溯歷史庫存變化。后來我改成了單獨的庫存表按倉庫加商品維度存數(shù)量即每條記錄對應(yīng)“某商品在某倉庫的庫存量”。這樣設(shè)計的好處有三個。一是多倉庫場景下庫存數(shù)據(jù)天然隔離每個倉庫各算各的二是盤點時只需要鎖定庫存表不需要動商品主數(shù)據(jù)三是后續(xù)如果要按倉庫維度統(tǒng)計庫存金額直接查庫存表關(guān)聯(lián)商品單價即可。入庫明細(xì)表和出庫明細(xì)表則把每一次操作的數(shù)量、單價、經(jīng)辦人、時間都記錄下來形成完整的溯源鏈路。3.3 功能模塊的邊界劃分整個系統(tǒng)我拆成了六個核心模塊商品管理、供應(yīng)商管理、倉庫管理、入庫管理、出庫管理、銷售管理。外加系統(tǒng)管理模塊負(fù)責(zé)用戶和權(quán)限配置統(tǒng)計報表模塊負(fù)責(zé)數(shù)據(jù)匯總。商品管理管的是商品檔案包括新增商品、編輯信息、上下架、條碼管理。供應(yīng)商管理維護供貨商檔案和合作記錄。倉庫管理管的是倉庫基本信息包括庫位編碼。入庫管理覆蓋采購入庫、退貨入庫、其它入庫這幾類場景。出庫管理覆蓋銷售出庫、報損出庫、調(diào)撥出庫等。這里我特別想說的是銷售管理和出庫管理的關(guān)系。有些系統(tǒng)把銷售訂單和出庫流程完全分開導(dǎo)致數(shù)據(jù)不一致。我的做法是銷售訂單創(chuàng)建后自動生成一條對應(yīng)的銷售出庫單庫存扣減以出庫單為準(zhǔn)確保銷售數(shù)據(jù)和庫存數(shù)據(jù)的聯(lián)動。這一塊邏輯看著簡單實際編碼時要處理并發(fā)問題后面我會詳細(xì)講。4. 核心業(yè)務(wù)流程與關(guān)鍵實現(xiàn)4.1 入庫流程的狀態(tài)流轉(zhuǎn)入庫流程的完整鏈路是創(chuàng)建入庫單 → 審核入庫單 → 更新庫存 → 記錄入庫明細(xì)。第一步倉庫人員根據(jù)采購單或到貨通知創(chuàng)建入庫單填寫供應(yīng)商、入庫倉庫、商品明細(xì)和數(shù)量。這里有一個容易忽略的地方入庫單的編號一定要系統(tǒng)自動生成采用日期加流水號的格式。比如RK20250615001可讀性強后期對賬、客服查詢都會方便很多。第二步是審核。我設(shè)置了一個原則創(chuàng)建人和審核人不能是同一人。入庫單保存后處于待審核狀態(tài)審核通過后才會真正影響庫存。這個設(shè)計是為了防止操作失誤被直接寫入庫存給復(fù)核留出空間。第三步和第四步是同步執(zhí)行的。審核通過的事務(wù)里先按“倉庫加商品”維度去庫存表查是否存在記錄。存在則增加數(shù)量不存在則先新增一條庫存記錄再累加。同時把每一件商品的入庫明細(xì)寫入入庫明細(xì)表。4.2 庫存扣減的并發(fā)控制方案庫存超賣是倉儲系統(tǒng)最敏感的問題。假設(shè)某商品庫存只剩10件同時來了兩個銷售訂單一單要8件一單要5件如果不做控制最終可能賣出去13件庫存變成負(fù)數(shù)。我用的方案是數(shù)據(jù)庫悲觀鎖配合事務(wù)隔離。執(zhí)行庫存扣減的SQL語句時用SELECT * FROM stock WHERE product_id ? AND warehouse_id ? FOR UPDATE先把這一行鎖住然后再檢查庫存是否充足執(zhí)行扣減。這樣同一時刻只有一個事務(wù)能操作這條庫存記錄徹底避免超賣。這里分享一個性能層面的小經(jīng)驗。悲觀鎖在并發(fā)量不是極大的場景下完全夠用零售倉儲系統(tǒng)的并發(fā)量一般不會像秒殺那么夸張。如果后續(xù)量大了可以改用Redis的分布式鎖或者樂觀鎖版本號機制。但雙十一那種級別的并發(fā)這個架構(gòu)本身就不合適需要引入消息隊列做異步削峰那就是另一套設(shè)計了。4.3 銷售與出庫的聯(lián)動實現(xiàn)用戶在前臺下單后系統(tǒng)會做這幾步操作校驗庫存是否充足、生成銷售訂單、生成出庫單、扣減庫存。這四個步驟我放在同一個事務(wù)里。一旦扣減庫存時發(fā)現(xiàn)異常整個事務(wù)回滾銷售訂單也不會生效。有一個實際問題需要防范用戶下單后沒有付款訂單處于待付款狀態(tài)這個時候就扣庫存用戶又不買了庫存就被白白鎖死。我需要區(qū)分普通電商和零售門店場景。如果是門店收銀基本上是現(xiàn)場付款直接扣庫存問題不大。但如果系統(tǒng)將來要支持線上預(yù)訂建議把扣減時間延后到支付成功之后或者引入預(yù)占庫存和釋放庫存的機制。4.4 盤點模塊的盈虧處理盤點這塊我覺得最能體現(xiàn)一個系統(tǒng)是否真正貼合業(yè)務(wù)場景。實際盤點時業(yè)務(wù)人員先錄入賬面數(shù)量再錄入實盤數(shù)量。系統(tǒng)自動計算盈虧數(shù)量。如果實盤數(shù)大于賬面數(shù)說明盤盈需要做盤盈入庫。如果實盤數(shù)小于賬面數(shù)說明盤虧需要做盤虧出庫。這一進(jìn)一出都要生成對應(yīng)的出入庫單據(jù)并且記錄盈虧原因。我在系統(tǒng)里預(yù)設(shè)了幾種常見原因掃碼遺漏、貨物損壞、供應(yīng)商少發(fā)、錄入錯誤。這樣財務(wù)復(fù)核時能直接看到原因歸類省去很多溝通成本。盤點單還有一個狀態(tài)進(jìn)行中、已提交、已審核。已經(jīng)審核的盤點單不可修改確實是復(fù)盤后確認(rèn)的數(shù)據(jù)。這個狀態(tài)機的控制保證了盤點流程的嚴(yán)肅性。5. 實操記錄與關(guān)鍵配置明細(xì)5.1 環(huán)境準(zhǔn)備與項目初始化項目開發(fā)環(huán)境我用的JDK 1.8Spring Boot 2.5版本。雖然JDK 17和Spring Boot 3.0都已經(jīng)很成熟但考慮到很多生產(chǎn)環(huán)境的服務(wù)器還在用JDK 8而且相關(guān)生態(tài)的兼容性問題少我最終選了相對保守的版本組合。用Spring Initializr初始化項目時我勾選了Web、MySQL Driver、MyBatis、Redis這幾個依賴。數(shù)據(jù)庫方面本地先用MySQL 5.7后續(xù)生產(chǎn)切換到MySQL 8.0連接驅(qū)動升級一下即可代碼改動很小。字符集一定要設(shè)utf8mb4否則存emoji或者特殊符號容易報錯。5.2 核心配置文件示例application.yml里的幾個關(guān)鍵配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/retail_storage?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0這里有幾個細(xì)節(jié)值得展開說。map-underscore-to-camel-case設(shè)為true后數(shù)據(jù)庫的create_time字段能自動映射到實體的createTime屬性省去手動寫映射規(guī)則的麻煩。邏輯刪除配置也很有用我沒用物理刪除所有業(yè)務(wù)表都加了deleted字段。用戶誤刪商品檔案時實際上數(shù)據(jù)還在只是查詢時被過濾掉了隨時可以恢復(fù)。5.3 統(tǒng)一返回結(jié)果與全局異常處理這個部分強烈建議所有Spring Boot項目都做。我定義了一個通用的Result類包含code、message、data三個字段。所有Controller接口都返回這個類前端可以根據(jù)code判斷請求是否成功。狀態(tài)碼我用的是20000表示成功50000表示業(yè)務(wù)異常40100表示未登錄或登錄過期。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(20000); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(50000); result.setMessage(message); return result; } }全局異常處理用RestControllerAdvice注解實現(xiàn)。業(yè)務(wù)異常、參數(shù)校驗異常、數(shù)據(jù)庫異常分別處理統(tǒng)一包裝成Result對象返回。這樣還有一個深層好處前端拿到統(tǒng)一格式的響應(yīng)體后可以封裝一套通用的請求攔截邏輯遇到40100自動跳登錄頁遇到50000統(tǒng)一彈出提示。不用每個頁面各寫一套錯誤處理邏輯。5.4 登錄驗證與JWT令牌用戶登錄后我用JWTJSON Web Token生成登錄令牌存到Redis里同時設(shè)置過期時間。每次請求經(jīng)過攔截器時從請求頭取出令牌并校驗。使用JWT加Redis的組合既保留了JWT無狀態(tài)的特點又能通過Redis手動踢人下線。令牌的有效期設(shè)置要結(jié)合業(yè)務(wù)場景來考慮。零售門店的人員流動性大如果令牌一個月不過期員工離職后賬號仍然有效會帶來安全隱患。我設(shè)置的是8小時過期配合Redis里存的會話記錄超時后自動重新登錄。門店收銀員一天登錄一次體驗上也沒有問題。5.5 前端頁面的技術(shù)選型前端部分用了Thymeleaf加Bootstrap沒做前后端分離。為什么這么選因為這套系統(tǒng)的使用人群是門店店員和倉庫管理員他們需要的質(zhì)檢場景是登錄后用瀏覽器操作頁面不追求炫酷效果只追求簡單好用加載速度快。如果要做前后端分離我建議后端提供純JSON接口配合Vue或React做管理后臺。但這會引入跨域處理、Token存儲、構(gòu)建部署等一系列額外復(fù)雜度。對于這個項目規(guī)模服務(wù)端渲染的方案在開發(fā)效率和維護便利性上反而更有優(yōu)勢。6. 常見問題與排查技巧實錄6.1 數(shù)據(jù)庫連接池連接超時系統(tǒng)運行一段時間后偶爾會報Communications link failure錯誤。排查后發(fā)現(xiàn)是數(shù)據(jù)庫連接的閑置時間超過了MySQL的wait_timeout。解決辦法是在配置文件中設(shè)置連接池參數(shù)讓連接池定時檢查并回收失效連接。spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 30000 max-lifetime: 1800000 connection-test-query: SELECT 1這里max-lifetime設(shè)置成30分鐘要比MySQL的wait_timeout短一些確保連接被提前回收不至于被數(shù)據(jù)庫服務(wù)端斷開后繼續(xù)使用。6.2 精度問題導(dǎo)致金額對不上有同事反饋某個訂單的總金額差了一分錢。最后定位到是商品單價用了double類型計算產(chǎn)生浮點數(shù)誤差。這個問題的教訓(xùn)很直接涉及金額的計算絕不能用double或float。后來我把所有金額字段統(tǒng)一改成Decimal代碼里用BigDecimal計算再也沒出現(xiàn)過這種問題。BigDecimal計算時要注意構(gòu)造方法盡量用字符串參數(shù)比如new BigDecimal(19.90)不要直接用new BigDecimal(19.90)。后者由于二進(jìn)制浮點數(shù)表示問題得到的值往往不是精確的19.9。初學(xué)的時候容易在這里踩坑。6.3 MyBatis Plus的字段填充失效使用MyBatis Plus的自動填充功能時我在實體類的createTime字段上加了TableField(fill FieldFill.INSERT)注解但新增數(shù)據(jù)時字段值還是null。排查發(fā)現(xiàn)是MetaObjectHandler的實現(xiàn)類沒有交給Spring管理缺了Component注解。這個實現(xiàn)類里我重寫了insertFill和updateFill兩個方法分別填充創(chuàng)建時間和更新時間。這類問題通常在配置層面代碼編譯不報錯運行時才暴露排查起來需要細(xì)心對照官方文檔。6.4 并發(fā)測試中出現(xiàn)的庫存負(fù)數(shù)系統(tǒng)上線前做壓測并發(fā)開30個線程同時購買同一件商品日志里出現(xiàn)了庫存負(fù)數(shù)的情況。這說明單純在Service層判斷庫存是否充足是擋不住并發(fā)的。多個線程同時讀到庫存為10同時判斷10大于購買數(shù)量全部通過然后一起扣減庫存就變成負(fù)數(shù)了。我改用數(shù)據(jù)庫行級鎖之后同一商品同一倉庫的庫存行同一時刻只有一個事務(wù)能操作后面的線程必須等待前一個事務(wù)提交后才能讀到最新庫存。再加上扣減的SQL語句里帶上條件stock quantity雙重保障徹底解決了這個問題。6.5 高頻查詢的Redis緩存策略商品列表、分類信息這類數(shù)據(jù)變化頻率低、讀取頻率高非常適合放到Redis里緩存。我的做法是添加或修改商品時主動刪除相關(guān)緩存查詢時先查緩存緩存不存在時再查數(shù)據(jù)庫并回填緩存。這里要重點提醒一句緩存一定要設(shè)置過期時間不能是永久key。不設(shè)置過期時間的緩存一旦忘記主動清理數(shù)據(jù)不一致的問題會長期存在。時間設(shè)短了效果差設(shè)長了數(shù)據(jù)舊一般我會根據(jù)業(yè)務(wù)對實時性的要求調(diào)整商品列表設(shè)30分鐘登錄會話設(shè)8小時是比較常規(guī)的配置。6.6 常見問題速查表問題現(xiàn)象可能原因解決方案項目啟動失敗端口被占用8080端口被其它進(jìn)程占用使用netstat命令查找進(jìn)程并停止或修改配置端口點擊登錄無反應(yīng)登錄接口報500SQL語句有誤查看后端日志定位具體報錯行導(dǎo)入商品Excel亂碼文件編碼不是UTF-8讀取時指定字符集前端上傳時做編碼處理庫存列表查詢慢商品表數(shù)據(jù)量大未加索引在商品條碼、分類ID字段上建立索引報表數(shù)據(jù)與明細(xì)對不上統(tǒng)計SQL存在重復(fù)關(guān)聯(lián)先用子查詢?nèi)ブ卦訇P(guān)聯(lián)匯總表7. 部署上線與數(shù)據(jù)備份經(jīng)驗系統(tǒng)部署我用了最直接的方式Maven打包出Jar文件扔到服務(wù)器上運行。生產(chǎn)環(huán)境我用的是CentOS系統(tǒng)執(zhí)行nohup java -jar retail-storage.jar app.log 21 日志輸出到文件方便排查問題。關(guān)于日志級別我建議生產(chǎn)環(huán)境用info級別開發(fā)環(huán)境用debug級別。debug日志記錄的信息量很大如果生產(chǎn)環(huán)境開啟磁盤很容易被刷爆。同時要用logback的按天滾動策略保留最近30天的日志避免日志文件無限增長。數(shù)據(jù)庫備份這件事我吃過虧。項目上線初期某天誤操作批量修改了商品數(shù)據(jù)由于沒有備份最后只能手工逐條恢復(fù)花了好幾個小時。后來我寫了一個shell腳本每天凌晨兩點用mysqldump全量備份備份文件保留7天再配合Binlog做增量恢復(fù)。每隔兩周手動演練一次恢復(fù)流程確保備份真的能恢復(fù)而不是備份了個寂寞。8. 項目復(fù)盤與個人實操心得整個系統(tǒng)從設(shè)計到開發(fā)再到上線加起來用了大約六周時間?,F(xiàn)在回頭復(fù)盤這個項目的價值主要有三層。第一層是業(yè)務(wù)建模。零售倉儲雖然沒有特別深奧的算法但業(yè)務(wù)流程的梳理很考驗邏輯能力。入庫、出庫、盤點、銷售之間環(huán)環(huán)相扣的狀態(tài)流轉(zhuǎn)一開始我想得簡單畫完時序圖才發(fā)現(xiàn)各種邊界情況都要處理比如審核駁回后庫存如何處理、盤點期間能不能繼續(xù)出入庫等等。第二層是數(shù)據(jù)一致性的把握。庫存、金額、訂單這三類數(shù)據(jù)必須保證絕對準(zhǔn)確任何一個環(huán)節(jié)出錯后續(xù)的對賬和財務(wù)都會很痛苦。事務(wù)邊界、并發(fā)控制、唯一約束這些基本功在這個項目里體現(xiàn)得淋漓盡致。第三層是對用戶體驗的認(rèn)識。這個系統(tǒng)的使用者年齡跨度大很多員工對計算機操作并不熟練。所以我做頁面時盡量遵循了幾個原則一個頁面只做一件事操作按鈕文字直白比如“新增商品”“確認(rèn)出庫”不要用“執(zhí)行”這種模糊詞匯。重要的操作有二次確認(rèn)彈窗比如刪除商品、盤點審核。每一步操作后有明確的成功或失敗提示。這些看似瑣碎的設(shè)計反而讓系統(tǒng)在實際使用中推廣得很順利。最后再分享一個小技巧多崗位角色協(xié)同使用系統(tǒng)時操作日志的作用遠(yuǎn)比想象中重要。我在所有關(guān)鍵業(yè)務(wù)表上都加了創(chuàng)建人、創(chuàng)建時間、更新人、更新時間字段并且單獨建了一張操作日志表記錄誰在什么時間做了什么操作。上線后有一次盤點差異排查就是靠操作日志發(fā)現(xiàn)有人在非工作時段做過一次入庫單修改才找到數(shù)據(jù)對不上的原因。這個設(shè)計不需要太高深的技術(shù)但對運維和審計非常有價值。如果你準(zhǔn)備復(fù)刻這個項目強烈建議把這個細(xì)節(jié)加進(jìn)去。