超市進(jìn)銷存商城源碼:前后臺(tái)庫存打通與對(duì)賬實(shí)戰(zhàn))
簡介本資源為基于JSP與SSM框架的社區(qū)生活超市進(jìn)銷存購物商城管理系統(tǒng)畢業(yè)設(shè)計(jì)全套資料面向計(jì)算機(jī)相關(guān)專業(yè)需要完成課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)的學(xué)生以及希望學(xué)習(xí)SSM整合開發(fā)的初級(jí)開發(fā)者。系統(tǒng)采用MySQL數(shù)據(jù)庫、JDK1.8兼容Eclipse、MyEclipse、STS、IDEA等開發(fā)工具功能覆蓋管理員、供應(yīng)商、用戶三種角色包含個(gè)人中心、用戶管理、供應(yīng)商管理、商品類型與信息管理、商品進(jìn)貨退貨、訂單管理、銷售出庫、商品盤點(diǎn)、超市資訊及系統(tǒng)管理等模塊前臺(tái)提供首頁、商品信息與后臺(tái)跳轉(zhuǎn)入口。資源包內(nèi)含源碼、數(shù)據(jù)庫腳本、論文、答辯PPT、環(huán)境工具包及同框架項(xiàng)目的安裝教程說明文檔壓縮包為rar格式整體約51.55MB。目前已有33人學(xué)習(xí)下載適合需要完整賽題方案、可運(yùn)行代碼與配套文檔的讀者參考使用。1. 從一份 JSPSSM 源碼說起社區(qū)超市進(jìn)銷存商城到底在解決什么問題很多同學(xué)拿到「畢業(yè)設(shè)計(jì)jspSSM的社區(qū)生活超市進(jìn)銷存購物商城管理源碼含文檔含教程」這個(gè)題目時(shí)第一反應(yīng)是去搜一堆 jsp 源碼、進(jìn)銷存源碼、購物商城源碼然后拼一個(gè)能跑起來的殼子交差。但真正做過社區(qū)超市這類業(yè)務(wù)的人會(huì)告訴你它和普通電商最大的區(qū)別在于前臺(tái)是購物商城后臺(tái)是進(jìn)銷存兩者共用一套庫存和訂單數(shù)據(jù)。用戶下單買走兩瓶醬油后臺(tái)庫存要實(shí)時(shí)扣減采購入庫一批生鮮前臺(tái)商品的可售數(shù)量要同步變化。這個(gè)「前后臺(tái)數(shù)據(jù)打通」才是這類系統(tǒng)真正的技術(shù)難點(diǎn)也是答辯老師最愛追問的地方。這篇文章面向三類人正在做這個(gè)畢業(yè)設(shè)計(jì)的同學(xué)、想用 SSM 快速搭一套小型進(jìn)銷存商城的開發(fā)者、以及需要給社區(qū)超市做一套輕量管理系統(tǒng)的從業(yè)者。我會(huì)把 JSPSSM 這套技術(shù)棧在進(jìn)銷存商城場景下的選型理由、數(shù)據(jù)庫設(shè)計(jì)、核心模塊實(shí)現(xiàn)、以及我踩過的坑按能復(fù)現(xiàn)的順序講清楚。你跟著走完能拿到一套結(jié)構(gòu)清晰、能跑通下單扣庫存全流程的工程骨架而不是一個(gè)只能看不能用的演示頁面。2. 技術(shù)選型與工程骨架為什么是 JSPSSM 而不是別的2.1 在 2024 年還選 JSPSSM 的四個(gè)現(xiàn)實(shí)理由先說清楚JSPSSMSpring SpringMVC MyBatis在今天不是最時(shí)髦的組合前端早就是 Vue/React 的天下了。但放到畢業(yè)設(shè)計(jì)和中小型社區(qū)超市系統(tǒng)這個(gè)具體場景里它有幾個(gè)繞不開的優(yōu)勢。第一是學(xué)習(xí)成本可控。SSM 三層架構(gòu)Controller-Service-DAO的職責(zé)劃分非常清晰MyBatis 把 SQL 寫在 XML 里對(duì)于要講清楚「數(shù)據(jù)怎么從頁面流到數(shù)據(jù)庫」的答辯場景比 MyBatis-Plus 那種全自動(dòng) ORM 更容易解釋。第二是資料密度高遇到問題搜 jsp、mybatis 源碼、springframework 源碼運(yùn)行這類關(guān)鍵詞能找到大量可參考的實(shí)現(xiàn)。第三是部署簡單打成一個(gè) war 包丟進(jìn) Tomcat 就能跑不需要 Node 環(huán)境、不需要前后端分離的跨域配置。第四是文檔和教程配套完整這類題目通常要求含文檔含教程SSM 的配置流程是標(biāo)準(zhǔn)化的寫文檔時(shí)不容易卡殼。需要提醒的是如果你的目標(biāo)是做一個(gè)真正上線的社區(qū)團(tuán)購系統(tǒng)那 SSM 的并發(fā)能力和開發(fā)效率都不夠看應(yīng)該考慮 SpringBoot Vue。但作為畢業(yè)設(shè)計(jì)SSM 的性價(jià)比是最高的。2.2 工程目錄結(jié)構(gòu)與依賴清單一個(gè)能跑通的 SSM 進(jìn)銷存商城標(biāo)準(zhǔn)目錄結(jié)構(gòu)如下。我一般會(huì)按功能模塊分包而不是按技術(shù)層分包這樣后期加功能時(shí)不用在四個(gè)包里來回跳。community-market/ ├── src/main/java/com/market/ │ ├── controller/ # 頁面請求入口 │ │ ├── GoodsController.java │ │ ├── OrderController.java │ │ └── StockController.java │ ├── service/ # 業(yè)務(wù)邏輯 │ │ ├── GoodsService.java │ │ └── OrderService.java │ ├── dao/ # MyBatis Mapper 接口 │ ├── entity/ # 實(shí)體類 │ └── util/ # 工具類 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML 映射文件 │ ├── spring-dao.xml │ ├── spring-service.xml │ ├── spring-mvc.xml │ └── jdbc.properties ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── jsp/ # 所有 JSP 頁面 │ └── static/ # css/js/圖片 └── pom.xmlMaven 依賴的核心部分注意版本要統(tǒng)一Spring 5.x 配 MyBatis 3.5.x 是經(jīng)過驗(yàn)證的穩(wěn)定組合!-- pom.xml 核心依賴 -- dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency !-- MyBatis 與 Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.29/version /dependency !-- JSP 相關(guān) -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies這段依賴?yán)飉ybatis-spring是整合的關(guān)鍵它負(fù)責(zé)把 MyBatis 的 SqlSession 交給 Spring 管理。mysql-connector-java用 8.x 版本時(shí)JDBC URL 必須加時(shí)區(qū)參數(shù)否則啟動(dòng)就報(bào)時(shí)區(qū)錯(cuò)誤這是新手最常見的翻車點(diǎn)之一。2.3 數(shù)據(jù)庫表設(shè)計(jì)進(jìn)銷存和商城如何共用一套庫存這是整個(gè)系統(tǒng)設(shè)計(jì)的核心。很多人把商城和進(jìn)銷存做成兩套表結(jié)果庫存對(duì)不上。正確做法是商品表只有一張庫存字段掛在商品上所有出入庫都通過庫存流水表記錄。表名作用關(guān)鍵字段goods商品主表id, name, price, stock, category_idstock_record庫存流水id, goods_id, type(入庫/出庫), num, create_timeorders訂單主表id, user_id, total_price, statusorder_item訂單明細(xì)id, order_id, goods_id, num, pricepurchase采購單id, goods_id, num, supplier, status庫存扣減的邏輯必須放在 Service 層用事務(wù)包起來先查庫存夠不夠再扣減再寫流水三步要么全成功要么全回滾。下面這段是下單扣庫存的核心代碼// OrderServiceImpl.java 下單扣庫存 Transactional(rollbackFor Exception.class) public void createOrder(Order order, ListOrderItem items) { for (OrderItem item : items) { // 1. 查詢商品當(dāng)前庫存加行鎖防止并發(fā)超賣 Goods goods goodsMapper.selectForUpdate(item.getGoodsId()); if (goods.getStock() item.getNum()) { throw new RuntimeException(庫存不足 goods.getName()); } // 2. 扣減庫存 goodsMapper.reduceStock(item.getGoodsId(), item.getNum()); // 3. 寫庫存流水 StockRecord record new StockRecord(); record.setGoodsId(item.getGoodsId()); record.setType(OUT); record.setNum(item.getNum()); stockRecordMapper.insert(record); } // 4. 寫訂單主表和明細(xì) orderMapper.insert(order); orderItemMapper.batchInsert(items); }selectForUpdate是關(guān)鍵它在 SQL 層加了行鎖防止兩個(gè)用戶同時(shí)下單把庫存扣成負(fù)數(shù)。Transactional的rollbackFor Exception.class保證任何異常都回滾默認(rèn)只回滾 RuntimeException加上這個(gè)更保險(xiǎn)。庫存流水表的作用是后悔藥一旦發(fā)現(xiàn)庫存對(duì)不上可以順著流水一條條查是哪一步出的問題。3. 前臺(tái)購物商城模塊從商品列表到下單支付3.1 商品列表與詳情頁的 JSP 實(shí)現(xiàn)前臺(tái)頁面用 JSP JSTL 渲染數(shù)據(jù)由 Controller 塞進(jìn) Model 再轉(zhuǎn)發(fā)到 JSP。商品列表頁的核心是把分類篩選和分頁做出來這是答辯時(shí)容易被問的點(diǎn)。// GoodsController.java 商品列表 RequestMapping(/goods/list) public String list(RequestParam(defaultValue 1) Integer page, RequestParam(required false) Integer categoryId, Model model) { // 每頁 12 條社區(qū)超市商品不多不用太大 PageHelper.startPage(page, 12); ListGoods goodsList goodsService.listByCategory(categoryId); PageInfoGoods pageInfo new PageInfo(goodsList); model.addAttribute(pageInfo, pageInfo); model.addAttribute(categoryId, categoryId); return goods/list; }這里用了 PageHelper 插件做分頁startPage必須緊跟在查詢方法前一行中間不能插別的查詢否則分頁會(huì)串。JSP 頁面里用c:forEach遍歷pageInfo.list用c:if判斷庫存為 0 時(shí)顯示「已售罄」。商品圖片的路徑建議存相對(duì)路徑比如/static/upload/xxx.jpg不要存絕對(duì)路徑換臺(tái)機(jī)器部署就找不到圖了。3.2 購物車與訂單提交的完整鏈路購物車有兩種實(shí)現(xiàn)存 Session 或存數(shù)據(jù)庫。社區(qū)超市這種場景我建議存數(shù)據(jù)庫因?yàn)橛脩艨赡軗Q設(shè)備而且訂單提交時(shí)要校驗(yàn)購物車數(shù)據(jù)存庫更可靠。購物車表結(jié)構(gòu)很簡單cart(id, user_id, goods_id, num, create_time)。加入購物車時(shí)先查是否已存在存在就累加數(shù)量不存在就插入。提交訂單時(shí)從購物車讀出所有條目逐條校驗(yàn)庫存然后調(diào)用第 2 章寫的createOrder方法。// CartController.java 提交訂單 RequestMapping(/order/submit) public String submit(HttpSession session, Model model) { User user (User) session.getAttribute(loginUser); if (user null) { return redirect:/user/login; } ListCart cartList cartService.listByUser(user.getId()); if (cartList.isEmpty()) { model.addAttribute(msg, 購物車為空); return cart/list; } try { orderService.createOrderFromCart(user.getId(), cartList); cartService.clearByUser(user.getId()); // 清空購物車 return redirect:/order/list; } catch (RuntimeException e) { model.addAttribute(msg, e.getMessage()); return cart/list; } }注意createOrderFromCart內(nèi)部要把 Cart 轉(zhuǎn)成 OrderItem價(jià)格從商品表實(shí)時(shí)讀取不要用購物車?yán)锏膬r(jià)格否則商品調(diào)價(jià)后訂單金額就錯(cuò)了。清空購物車要放在訂單創(chuàng)建成功之后如果放前面訂單失敗購物車就丟了用戶得重新加一遍體驗(yàn)很差。3.3 訂單狀態(tài)流轉(zhuǎn)與庫存回滾訂單狀態(tài)一般有待付款、已付款、已發(fā)貨、已完成、已取消。社區(qū)超市可以簡化去掉發(fā)貨環(huán)節(jié)改成待付款、待取貨、已完成、已取消。關(guān)鍵點(diǎn)是取消訂單時(shí)要回滾庫存?;貪L邏輯和扣減相反加回庫存、寫一條 IN 類型的流水。這里有個(gè)坑如果訂單已經(jīng)付款了再取消涉及退款畢業(yè)設(shè)計(jì)里可以簡化成只允許待付款狀態(tài)取消。// OrderServiceImpl.java 取消訂單回滾庫存 Transactional(rollbackFor Exception.class) public void cancelOrder(Integer orderId) { Order order orderMapper.selectById(orderId); if (!UNPAID.equals(order.getStatus())) { throw new RuntimeException(只有待付款訂單可以取消); } ListOrderItem items orderItemMapper.listByOrder(orderId); for (OrderItem item : items) { goodsMapper.addStock(item.getGoodsId(), item.getNum()); StockRecord record new StockRecord(); record.setGoodsId(item.getGoodsId()); record.setType(IN); record.setNum(item.getNum()); stockRecordMapper.insert(record); } orderMapper.updateStatus(orderId, CANCELED); }狀態(tài)判斷用字符串常量建議定義成枚舉或常量類不要到處寫魔法字符串改起來容易漏。4. 后臺(tái)進(jìn)銷存模塊采購入庫、庫存預(yù)警與數(shù)據(jù)看板4.1 采購入庫流程與庫存流水記錄進(jìn)銷存的核心是「進(jìn)」和「銷」。進(jìn)貨就是采購入庫流程是創(chuàng)建采購單 → 審核 → 入庫 → 更新商品庫存 → 寫流水。采購單表purchase(id, goods_id, num, supplier, status, create_time)status 有「待審核」「已入庫」。入庫操作要在一個(gè)事務(wù)里完成三件事改采購單狀態(tài)、加商品庫存、寫庫存流水。// StockServiceImpl.java 采購入庫 Transactional(rollbackFor Exception.class) public void purchaseIn(Integer purchaseId) { Purchase purchase purchaseMapper.selectById(purchaseId); if (!PENDING.equals(purchase.getStatus())) { throw new RuntimeException(該采購單已入庫或狀態(tài)異常); } // 1. 加庫存 goodsMapper.addStock(purchase.getGoodsId(), purchase.getNum()); // 2. 寫流水 StockRecord record new StockRecord(); record.setGoodsId(purchase.getGoodsId()); record.setType(IN); record.setNum(purchase.getNum()); record.setRemark(采購入庫單號(hào) purchaseId); stockRecordMapper.insert(record); // 3. 改采購單狀態(tài) purchaseMapper.updateStatus(purchaseId, FINISHED); }remark字段建議一定要留把采購單號(hào)寫進(jìn)去對(duì)賬時(shí)能直接定位。我見過有人把流水表設(shè)計(jì)得只有 goods_id 和 num出了問題完全查不出是哪筆業(yè)務(wù)導(dǎo)致的。4.2 庫存預(yù)警的閾值設(shè)置與定時(shí)檢查社區(qū)超市最怕的是貨賣空了還不知道。庫存預(yù)警的做法是給商品表加一個(gè)warn_stock字段表示預(yù)警閾值。當(dāng)庫存低于這個(gè)值時(shí)后臺(tái)首頁標(biāo)紅提示。檢查方式有兩種一是每次扣減庫存后判斷實(shí)時(shí)性高但代碼侵入性強(qiáng)二是用 Spring 的定時(shí)任務(wù)每隔一段時(shí)間掃一遍。我一般用第二種配置簡單對(duì)業(yè)務(wù)代碼無侵入。// StockWarnTask.java 定時(shí)檢查庫存預(yù)警 Component public class StockWarnTask { Autowired private GoodsMapper goodsMapper; // 每天早上 8 點(diǎn)和下午 6 點(diǎn)各檢查一次 Scheduled(cron 0 0 8,18 * * ?) public void checkWarnStock() { ListGoods warnList goodsMapper.listWarnGoods(); if (!warnList.isEmpty()) { // 實(shí)際項(xiàng)目里這里發(fā)郵件或站內(nèi)信畢業(yè)設(shè)計(jì)打印日志即可 System.out.println(庫存預(yù)警商品數(shù)量 warnList.size()); } } }Scheduled需要在 spring 配置里開啟task:annotation-driven/cron 表達(dá)式0 0 8,18 * * ?表示每天 8 點(diǎn)和 18 點(diǎn)整執(zhí)行。閾值warn_stock不要設(shè)成固定值生鮮類可以設(shè) 20日用品設(shè) 5按品類區(qū)分更合理。4.3 銷售統(tǒng)計(jì)看板用 SQL 聚合代替 Java 循環(huán)后臺(tái)首頁一般要展示今日銷售額、熱銷商品 Top10、庫存總量這些指標(biāo)。新手容易犯的錯(cuò)是把所有訂單查出來在 Java 里循環(huán)累加數(shù)據(jù)一多就慢。正確做法是用 SQL 聚合。-- 今日銷售額統(tǒng)計(jì) SELECT IFNULL(SUM(total_price), 0) AS todayAmount FROM orders WHERE status FINISHED AND DATE(create_time) CURDATE(); -- 熱銷商品 Top10 SELECT g.name, SUM(oi.num) AS saleNum FROM order_item oi JOIN goods g ON oi.goods_id g.id JOIN orders o ON oi.order_id o.id WHERE o.status FINISHED GROUP BY oi.goods_id ORDER BY saleNum DESC LIMIT 10;IFNULL是為了防止當(dāng)天沒有訂單時(shí)返回 null 導(dǎo)致頁面報(bào)錯(cuò)。DATE(create_time) CURDATE()這種寫法用不上索引數(shù)據(jù)量大時(shí)要改成范圍查詢create_time CURDATE() AND create_time CURDATE() INTERVAL 1 DAY。畢業(yè)設(shè)計(jì)數(shù)據(jù)量小無所謂但養(yǎng)成好習(xí)慣沒壞處。5. 避坑與排查這套源碼跑不起來時(shí)先看這幾條5.1 啟動(dòng)報(bào) 404頁面就是出不來現(xiàn)象Tomcat 啟動(dòng)沒報(bào)錯(cuò)訪問首頁返回 404。原因通常是web.xml里 DispatcherServlet 的映射配錯(cuò)或者spring-mvc.xml的組件掃描路徑?jīng)]覆蓋到 Controller。解決檢查web.xml中url-pattern是不是/檢查context:component-scan base-packagecom.market.controller/的包名和實(shí)際目錄是否一致。還有一個(gè)隱蔽原因是 JSP 放在WEB-INF下但視圖解析器前綴寫錯(cuò)/WEB-INF/jsp/和/WEB-INF/views/差一個(gè)字母就 404。5.2 庫存扣成負(fù)數(shù)并發(fā)下單出問題現(xiàn)象壓測或多人同時(shí)下單時(shí)庫存出現(xiàn)負(fù)數(shù)。原因是沒有加鎖兩個(gè)線程同時(shí)查到庫存為 1都判斷夠都扣減。解決在查詢庫存的 SQL 上加FOR UPDATE或者用UPDATE goods SET stock stock - #{num} WHERE id #{id} AND stock #{num}這種帶條件的更新根據(jù)返回的影響行數(shù)判斷是否成功。后者性能更好推薦用。5.3 MyBatis 查詢返回 null字段名對(duì)不上現(xiàn)象數(shù)據(jù)庫明明有數(shù)據(jù)查詢返回的對(duì)象字段全是 null。原因九成是數(shù)據(jù)庫字段名和實(shí)體類屬性名不一致比如數(shù)據(jù)庫是create_time實(shí)體類是createTime而 MyBatis 沒開啟駝峰映射。解決在mybatis-config.xml里加setting namemapUnderscoreToCamelCase valuetrue/或者在 SQL 里用AS起別名。我一般兩個(gè)都做雙保險(xiǎn)。5.4 中文亂碼從頁面到數(shù)據(jù)庫全是問號(hào)現(xiàn)象表單提交的中文存進(jìn)數(shù)據(jù)庫變成???。原因分三處JSP 頁面編碼、請求編碼、數(shù)據(jù)庫連接編碼。解決JSP 頂部加% page contentTypetext/html;charsetUTF-8 %web.xml里配 CharacterEncodingFilter 設(shè)成 UTF-8JDBC URL 加useUnicodetruecharacterEncodingutf8。三處缺一處都可能亂碼按順序排查。5.5 事務(wù)不生效庫存扣了訂單沒生成現(xiàn)象下單時(shí)庫存扣了但訂單表沒數(shù)據(jù)或者反過來。原因通常是Transactional加在了 Controller 上或者同類內(nèi)部方法調(diào)用繞過了代理。解決事務(wù)注解必須加在 Service 層的 public 方法上且這個(gè)方法要被外部調(diào)用。如果 Service 內(nèi)部 A 方法調(diào) B 方法B 的事務(wù)不會(huì)生效要么把 B 抽到另一個(gè) Service要么用 AopContext 拿代理對(duì)象。6. 進(jìn)階技巧把庫存流水做成可追溯的對(duì)賬工具前面幾章把系統(tǒng)跑通了這一章講一個(gè)能讓你的畢業(yè)設(shè)計(jì)在答辯時(shí)加分、實(shí)際用起來也真正有價(jià)值的技巧用庫存流水表做一套完整的對(duì)賬機(jī)制。大部分人的庫存流水只是記一下出入庫查完就完了。但流水表真正的價(jià)值在于「任意時(shí)間點(diǎn)的庫存還原」。公式很簡單某時(shí)刻庫存 期初庫存 期間所有 IN 流水之和 - 期間所有 OUT 流水之和。把這個(gè)公式做成一個(gè)對(duì)賬頁面輸入起止日期系統(tǒng)自動(dòng)算出理論庫存再和商品表當(dāng)前庫存對(duì)比不一致就說明有 bug 或者有人直接改了數(shù)據(jù)庫。// StockServiceImpl.java 庫存對(duì)賬 public StockCheckVO checkStock(Integer goodsId, String startDate, String endDate) { // 期初庫存起始日期之前的流水匯總 Integer initStock stockRecordMapper.sumBeforeDate(goodsId, startDate); // 期間入庫總量 Integer inNum stockRecordMapper.sumByTypeAndRange(goodsId, IN, startDate, endDate); // 期間出庫總量 Integer outNum stockRecordMapper.sumByTypeAndRange(goodsId, OUT, startDate, endDate); // 理論庫存 int theoryStock (initStock null ? 0 : initStock) inNum - outNum; // 實(shí)際庫存 int realStock goodsMapper.selectById(goodsId).getStock(); StockCheckVO vo new StockCheckVO(); vo.setTheoryStock(theoryStock); vo.setRealStock(realStock); vo.setDiff(theoryStock - realStock); return vo; }對(duì)應(yīng)的 SQL 用SUM和CASE WHEN就能搞定不需要在 Java 里循環(huán)-- 期初庫存起始日期前的入庫減出庫 SELECT IFNULL(SUM(CASE WHEN type IN THEN num ELSE -num END), 0) FROM stock_record WHERE goods_id #{goodsId} AND create_time #{startDate};這個(gè)對(duì)賬功能有三個(gè)實(shí)際好處。第一答辯時(shí)老師問「你怎么保證庫存數(shù)據(jù)準(zhǔn)確」你可以直接演示對(duì)賬頁面比空口說「我用了事務(wù)」有說服力得多。第二實(shí)際運(yùn)營中如果發(fā)現(xiàn)庫存對(duì)不上能快速定位是哪天的流水出了問題。第三這套思路可以擴(kuò)展到金額對(duì)賬訂單表的應(yīng)收和支付流水對(duì)不上時(shí)同樣適用。參數(shù)上要注意startDate和endDate建議用yyyy-MM-dd格式SQL 里用create_time #{startDate} AND create_time DATE_ADD(#{endDate}, INTERVAL 1 DAY)這樣能覆蓋 endDate 當(dāng)天的全部數(shù)據(jù)避免漏掉當(dāng)天下午的記錄。這個(gè)邊界坑我踩過當(dāng)時(shí)對(duì)賬差了 3 筆查了一下午才發(fā)現(xiàn)是日期范圍沒包含當(dāng)天。最后說個(gè)習(xí)慣。我做這類系統(tǒng)時(shí)任何改動(dòng)庫存的操作都必須寫流水沒有例外。哪怕是手動(dòng)在后臺(tái)改一個(gè)數(shù)字也要補(bǔ)一條備注為「手動(dòng)調(diào)整」的流水。這樣流水表就是庫存的唯一真相來源商品表的 stock 字段只是流水算出來的一個(gè)緩存值。養(yǎng)成這個(gè)習(xí)慣后庫存問題從「玄學(xué)」變成了「查日志」排查時(shí)間從半天縮短到幾分鐘。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取