:從技術(shù)棧判斷到小程序?qū)颖芸又改? alt=)
簡介基于ASP.NET與MVC三層架構(gòu)打造的商城系統(tǒng)完整源碼附帶小程序商城端面向需快速搭建或二次開發(fā)電商平臺的.NET開發(fā)人員同樣適用于高校畢設(shè)與商業(yè)項(xiàng)目起步參考。資源包共2000個文件以C#源文件、ASP.NET頁面、樣式腳本、交互JS及DLL組件為主并包含數(shù)據(jù)庫視圖與存儲過程資源包約127.65MB。目前已有799人學(xué)習(xí)下載。系統(tǒng)完整開源覆蓋會員等級積分、購物車、訂單管理、支付寶擔(dān)保交易、發(fā)貨確認(rèn)與收貨好評等主流電商功能采用B/S模式界面友好易操作內(nèi)置數(shù)據(jù)合法性校驗(yàn)與事務(wù)回滾機(jī)制以保障數(shù)據(jù)安全。對于研究.NET商城架構(gòu)、理解支付集成或進(jìn)行功能擴(kuò)展的開發(fā)者這份源碼提供了從代碼到數(shù)據(jù)庫腳本、存儲過程的完整落地實(shí)例目錄結(jié)構(gòu)清晰便于按模塊學(xué)習(xí)和調(diào)試。1. 一套 ASP.NET 商城源碼附贈小程序端這條鏈路到底幫你省了哪些活A(yù)SP.NET 商城源碼贈送小程序商城這種包在 .NET 開發(fā)者圈子里流傳很廣一套完整的電商后端商品、分類、購物車、訂單、會員、后臺管理都齊還白送一個微信小程序前端。對獨(dú)立開發(fā)者來說最值錢的不是后臺頁面數(shù)量而是從數(shù)據(jù)庫表設(shè)計(jì)到接口聯(lián)調(diào)、再到微信支付回調(diào)這一整條鏈路已經(jīng)有人替你走通。接手后的活從從零寫商城降級成部署、改配置、按業(yè)務(wù)改需求。這篇筆記按我接手這類源碼的順序講先判斷技術(shù)棧再跑通數(shù)據(jù)庫和本地環(huán)境最后收在小程序?qū)优c上線避坑。適合已經(jīng)拿到源碼包、準(zhǔn)備自己部署或二次開發(fā)的人。2. 拆包看架構(gòu)先確認(rèn) .NET Framework 還是 asp.net core再決定怎么接手拿到任何一個商城源碼包我第一件事不是解壓按 F5而是先看 .sln 和 .csproj。標(biāo)題里統(tǒng)稱ASP.NET實(shí)際可能是跨了十年的兩種技術(shù)路線判斷錯了后面全是血淚經(jīng)驗(yàn)老項(xiàng)目在 Windows 上能跑你非要往 Linux 上部署新項(xiàng)目命令行一條命令就能起你還在滿世界找 IIS 配置。先用三十分鐘把架構(gòu)看清楚比什么都重要。2.1 先看 TargetFramework三種常見形態(tài)一眼認(rèn)出來用記事本打開 .csproj 就能判斷。老一代的 .NET Framework 4.x 項(xiàng)目沒有 TargetFramework 節(jié)點(diǎn)引包靠 packages.config只能在 Windows 加 IIS Express 環(huán)境里跑新一代的 asp.net core 項(xiàng)目csproj 頂部會寫著 net6.0 或 net8.0能跨平臺能直接用dotnet run拉起來。還有第三類更頭疼說是 ASP.NET實(shí)際是 WebForm一堆 .aspx 頁面加服務(wù)端控件接口用 ashx 一般處理程序或者干脆把 aspx 當(dāng)接口輸出 JSON二次開發(fā)體驗(yàn)最差。判斷點(diǎn).NET Framework 老項(xiàng)目asp.net core 項(xiàng)目csproj 關(guān)鍵特征無 TargetFramework存在 packages.configTargetFramework 寫明 net6.0 / net8.0運(yùn)行方式Windows IIS / IIS Express命令行 dotnet run跨平臺常見出現(xiàn)時間2010–2017 年交付2018 年之后交付判斷順序三十秒先看有沒有 TargetFramework再看有沒有 packages.config最后掃一眼目錄里有沒有 .aspx 文件。很多號稱ASP.NET 商城源碼的包其實(shí)是 2015 年前后的 MVC 5 項(xiàng)目跑在 .NET Framework 4.5 上依賴 Newtonsoft.Json、Entity Framework 6 這些老版本NuGet 還原時最容易出問題。這一眼確認(rèn)完后面所有操作路徑都不一樣了。提示老 Framework 項(xiàng)目不是不能用于新業(yè)務(wù)但它的依賴還原、證書鏈、開發(fā)工具版本都可能成為坑。先確認(rèn)技術(shù)棧再談后面的改造方案。2.2 典型商城分層Model、DAL、BLL、Admin 與 Api 各自管什么這類源碼的分層高度雷同看懂了就能少走彎路。Model 層放實(shí)體類對應(yīng)數(shù)據(jù)庫表DAL 層是數(shù)據(jù)訪問用 SqlHelper、Dapper 或 EF 實(shí)現(xiàn)BLL 層管業(yè)務(wù)邏輯下單、扣庫存、算運(yùn)費(fèi)都在這里Web 層是門戶商城頁面Admin 是后臺管理這兩個有大量 Razor 或 aspx 視圖最后通常會有一個 Api 項(xiàng)目或 Api 區(qū)域?qū)iT給小程序端提供 JSON 接口。有個細(xì)節(jié)值得注意小程序接口和數(shù)據(jù)訪問層經(jīng)常不在一個項(xiàng)目里。有的源碼把 Api 做在 Admin 項(xiàng)目下路由寫成 /api/xxx和前臺上線在同一個站點(diǎn)。這種寫法省事但埋了隱患——后臺管理員的鑒權(quán)策略和小程序接口混在一起改后臺登錄邏輯時會把小程序接口一起帶崩。我一般會先確認(rèn) Api 是不是一個獨(dú)立可發(fā)布的項(xiàng)目如果不是二次開發(fā)時優(yōu)先把它拆出去。還要警惕鑒權(quán)寫得太隨意的源碼。有的商城源碼對小程序的商品、訂單接口根本不鑒權(quán)拉列表、查訂單、甚至看別人收貨地址都是裸奔狀態(tài)這類問題我放在第五章專門講。另外后臺管理頁基本都是 Bootstrap 老模板堆出來的樣式代碼比較亂想換皮可以拿 bootstrap studio 這類可視化工具重新拖界面但核心鏈路沒驗(yàn)證之前別折騰皮膚那不是當(dāng)前的主要矛盾。2.3 小程序端在包里長什么樣為什么它不在解決方案里所謂贈送的小程序商城通常是一個獨(dú)立文件夾名字叫 wxapp 或 mall-miniapp里面是原生小程序代碼pages/index、pages/goods、pages/cart、pages/order、utils/request.js 這一套。它不屬于 .sln 里的項(xiàng)目在 VS 里看不到得用微信開發(fā)者工具單獨(dú)打開。打開后第一件事是改接口地址。源碼作者通常留的是他自己的測試域名甚至是 http://localhost:5000 這種本地地址你不改的話小程序端一請求就失敗。把它改成自己部署好的 Api 地址然后看 utils/request.js 里有沒有統(tǒng)一的 baseUrl 配置。小程序端不能直接訪問 IP生產(chǎn)環(huán)境必須配 HTTPS 域名并且在小程序后臺把域名加進(jìn) request 合法域名列表這一步是硬性要求。如果包里根本沒有小程序端只在文檔里提了一句登錄接口是 /api/user/login那你拿到的就不是完整包。不用急著找作者扯皮按第四章的方案自己接一個原生小程序端也不復(fù)雜畢竟微信端的核心也就登錄、商品列表、下單、支付那幾條接口后端接口現(xiàn)成的。3. 數(shù)據(jù)庫初始化和本地跑通連接串、SQL 腳本與最小啟動清單后端代碼寫得再漂亮數(shù)據(jù)庫起不來都是零。商城源碼的數(shù)據(jù)庫絕大多數(shù)是 SQL Server少數(shù)改成了 MySQL。數(shù)據(jù)庫相關(guān)文件通常在根目錄的 db、database 或 sql 文件夾下有的是一個幾十 MB 的單文件 .sql有的是按結(jié)構(gòu)、初始數(shù)據(jù)、存儲過程拆成的多個腳本。拿到包先把這個文件夾完整看一遍搞清楚你面對的是哪種形態(tài)。3.1 SQL 腳本執(zhí)行順序結(jié)構(gòu)、種子數(shù)據(jù)、存儲過程一個文件翻車全鏈路斷先分清單文件還是多文件。單文件腳本理論上一次執(zhí)行就能建庫建表但實(shí)際中經(jīng)常翻車腳本頭部的IF DB_ID(MallDB) IS NULL CREATE DATABASE...在低權(quán)限賬號下執(zhí)行會直接報(bào)錯腳本中間如果不帶 GO 分隔符建表和插入語句混在同一個批次里SSMS 能過換成程序里的 ADO.NET 執(zhí)行就報(bào)對象名無效這兩類問題我都遇到過。多文件腳本就要注意順序了。常規(guī)順序是先建庫建表再插基礎(chǔ)字典數(shù)據(jù)分類、配送方式、支付方式再插測試商品數(shù)據(jù)最后跑存儲過程和視圖。最怕的是文件之間存在外鍵依賴先執(zhí)行的那批引用了后建的表直接報(bào)外鍵沖突。正確做法是先用文本編輯器打開每個腳本的頭幾行確認(rèn)是建庫建表的還是 INSERT 數(shù)據(jù)的分清楚再執(zhí)行。-- 常見建庫腳本開頭先查是否存在再建庫注意排序規(guī)則 IF DB_ID(MallDB) IS NULL BEGIN CREATE DATABASE MallDB COLLATE Chinese_PRC_CI_AS; END GO USE MallDB; GO -- 建表商品表金額用 DECIMAL 不用 float庫存用 INT CREATE TABLE dbo.Goods ( Id INT IDENTITY(1,1) PRIMARY KEY, GoodsName NVARCHAR(200) NOT NULL, Price DECIMAL(18,2) NOT NULL, Stock INT NOT NULL DEFAULT 0, ImgUrl NVARCHAR(500) NULL ); GO這段腳本的邏輯是先用IF DB_ID判斷避免重復(fù)建庫報(bào)錯COLLATE Chinese_PRC_CI_AS指定中文排序規(guī)則否則后面中文模糊查詢的結(jié)果可能不符合預(yù)期。Price用DECIMAL(18,2)而不是float是因?yàn)楦↑c(diǎn)類型在累計(jì)金額、對賬時會有精度誤差這在商城業(yè)務(wù)里不能忍。Stock用INT第五章我會講為什么扣庫存必須帶條件更新。注意腳本編碼是隱形殺手。UTF-8 無 BOM 的腳本導(dǎo)入到排序規(guī)則不對的實(shí)例中文數(shù)據(jù)會變成亂碼。SSMS 打開小文件一般沒事用命令行批量執(zhí)行時務(wù)必先確認(rèn)編碼。3.2 修改連接串web.config 與 appsettings.json 的差異老項(xiàng)目的連接串在 Web.config 的connectionStrings節(jié)點(diǎn)里Core 項(xiàng)目在 appsettings.json 的 ConnectionStrings 配置段。套路一樣但有個坑老源碼經(jīng)常把連接串同時寫在 web.config 和某個 DBHelper 類的構(gòu)造函數(shù)里改了一處漏了一處跑起來還是連不上。拿到包先全局搜索連接串字符串確認(rèn)一共有幾處需要改。!-- Web.config 老式寫法注意 MARS 參數(shù)老代碼很容易踩 -- connectionStrings add nameMallConn connectionStringServerlocalhost\SQLEXPRESS;DatabaseMallDB;User Idsa;PasswordYourPass123;MultipleActiveResultSetstrue; providerNameSystem.Data.SqlClient / /connectionStrings對應(yīng) asp.net core 項(xiàng)目配置長這樣{ ConnectionStrings: { MallConn: Serverlocalhost;DatabaseMallDB;User Idsa;PasswordYourPass123;TrustServerCertificatetrue; } }連接串里最常被忽略的參數(shù)是MultipleActiveResultSetstrue。老代碼經(jīng)常在同一個連接上先開 DataReader 再執(zhí)行另一個查詢沒有這個參數(shù)會隨機(jī)報(bào)已有打開的與此連接關(guān)聯(lián)的 DataReader屬于那種不報(bào)錯但時不時抽風(fēng)的典型問題。本地開發(fā)我習(xí)慣用 Windows 身份驗(yàn)證Integrated Securitytrue省去密碼配置發(fā)布到服務(wù)器再改成 SQL 賬號權(quán)限只給這個庫的讀寫別用 sa。3.3 啟動順序與驗(yàn)證先起 Api 再起 Web用瀏覽器和 curl 確認(rèn)跑通整個項(xiàng)目有固定順序先啟動 Api 項(xiàng)目確認(rèn)接口能返回 JSON再啟動 Web 門戶和 Admin 后臺最后才用微信開發(fā)者工具打開小程序端。原因很簡單小程序端所有頁面都依賴接口Api 沒起來前端怎么調(diào)都是白屏你還會誤以為是前端代碼有問題。Framework 項(xiàng)目在 VS 里右鍵 Api 項(xiàng)目設(shè)為啟動項(xiàng)目F5 用 IIS Express 跑asp.net core 項(xiàng)目可以直接命令行# asp.net core 項(xiàng)目先還原依賴再指定端口啟動 cd src/Mall.Api dotnet restore dotnet run --urls http://localhost:5000啟動后用 curl 驗(yàn)證一個不需要登錄的接口最常見的健康檢查是商品列表curl -s http://localhost:5000/api/goods/list | head -c 500如果返回一串 JSON 而不是錯誤頁說明數(shù)據(jù)庫連接、路由、模型綁定都是通的。返回的 JSON 里注意看code字段很多源碼的約定是{ code: 0, data: [...] }code非 0 說明業(yè)務(wù)層有錯。這時看控制臺輸出或 IIS Express 日志多半能定位到是哪條 SQL 報(bào)錯優(yōu)先檢查表名和字段名跟實(shí)體類是否對得上老源碼里命名不一致的情況很常見。4. 小程序商城對接wx.login 登錄態(tài)、請求封裝與支付回調(diào)驗(yàn)簽小程序商城的技術(shù)含量集中在三條鏈路登錄、請求鑒權(quán)、支付回調(diào)。這三條做不對商品頁做得再好看也白搭。這一章全部給可抄的代碼但有個前提你要先想清楚這套源碼的小程序端如果是后來補(bǔ)的接口設(shè)計(jì)可能跟原生商城前端不一樣對接之前先把 Api 項(xiàng)目里的控制器列表過一遍確認(rèn)有哪些接口可用。4.1 wx.login 換 openid服務(wù)端換 token別把 AppSecret 給前端小程序里沒有 Cookie 概念唯一的身份錨點(diǎn)是微信的 openid。標(biāo)準(zhǔn)流程是wx.login 拿到臨時 code傳給服務(wù)器服務(wù)器拿 code 加上 AppID 和 AppSecret 去微信接口換 openid 和 session_key。AppSecret 是這家商城的命根子一旦泄漏別人能用你的 AppID 做任何事所以這串東西只能存在服務(wù)端配置里絕不能寫進(jìn)小程序代碼。// 服務(wù)端code 換 openid再生成業(yè)務(wù) token [Route(api/wx/login)] public async TaskIActionResult Login(string code) { // 微信官方接口js_code 是一次性的兩分鐘內(nèi)有效 var url $https://api.weixin.qq.com/sns/jscode2session? $appid{_wxAppId}secret{_wxSecret}js_code{code}grant_typeauthorization_code; var json await _httpClient.GetStringAsync(url); var res JsonConvert.DeserializeObjectWxSession(json); if (string.IsNullOrEmpty(res.OpenId)) { // errcode 40029 表示 code 無效多半是前端用了過期 code return Json(new { code 400, msg res.ErrMsg }); } // 生成自己的 tokenopenid 不要直接下發(fā)防止被偽裝身份 var token Guid.NewGuid().ToString(N); _cache.Set($mall_token_{token}, res.OpenId, TimeSpan.FromDays(7)); return Json(new { code 0, data new { token } }); }這段代碼的關(guān)鍵參數(shù)jscode2session的 code 有效期只有五分鐘且只能用一次前端重放同一個 code 必然失敗所以失敗時應(yīng)該讓前端重新走wx.login。token 用 GUID 直拼看起來簡陋但對大部分商城夠用了想更嚴(yán)謹(jǐn)就把 token 綁到用戶表的主鍵上存 Redis 并設(shè)置過期時間后端每次請求查一次緩存驗(yàn)證。緩存過期時間我習(xí)慣設(shè) 7 天跟微信的 session_key 有效期對齊太短會導(dǎo)致用戶頻繁重新登錄。4.2 請求封裝小程序端不認(rèn) Cookie用 header 存 token 統(tǒng)一處理 401老商城源碼如果是靠 Session 認(rèn)人的小程序端一接就翻車——因?yàn)閣x.request不會像瀏覽器那樣自動攜帶 Cookie更不會幫你記住服務(wù)器種下的 Set-Cookie。這是老 ASP.NET 項(xiàng)目對接小程序的經(jīng)典事故現(xiàn)場我在第五章會再講一個具體案例。對接老源碼的第一件事就是把登錄態(tài)從 Session 改成 token 模式。// 小程序端 utils/request.js統(tǒng)一 baseUrl、統(tǒng)一帶 token、統(tǒng)一處理 401 const BASE_URL https://yourmall.com; function request(path, data, method GET) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { Authorization: Bearer wx.getStorageSync(token) }, success(res) { // 約定code 非 0 是業(yè)務(wù)失敗401 是登錄態(tài)過期 if (res.statusCode 401 || (res.data res.data.code 401)) { // 先清掉過期 token再重新走一遍 wx.login 并重放當(dāng)前請求 wx.removeStorageSync(token); reLogin().then(() request(path, data, method).then(resolve, reject)); return; } resolve(res.data); }, fail: reject }); }); } function reLogin() { return new Promise((resolve, reject) { wx.login({ success(res) { request(/api/wx/login, { code: res.code }, POST) .then(r { wx.setStorageSync(token, r.data.token); resolve(); }) .catch(reject); } }); }); }這個封裝解決了三個問題token 統(tǒng)一注入避免每個頁面寫一遍 header401 自動重登用戶無感續(xù)期所有請求走 Promise頁面代碼可以放心用 async/await。代價(jià)是reLogin里調(diào)用了request本身如果登錄接口自身返回 401 會死循環(huán)所以登錄接口的返回約定要單獨(dú)處理一般把它的 code 定為 0 或 400不要用 401。4.3 微信支付回調(diào)先驗(yàn)簽、比金額、做冪等再更新訂單支付是商城最不能出錯的環(huán)節(jié)。小程序端調(diào)wx.requestPayment之前服務(wù)器要先調(diào)微信統(tǒng)一下單接口拿到 prepay_id 和簽名參數(shù)。下單那一段各源碼實(shí)現(xiàn)差異很大但回調(diào)驗(yàn)簽這一段是通用的直接決定你能不能安全收款?;卣{(diào)里拿到的信息必須跟訂單表做交叉驗(yàn)證這才是防篡改的關(guān)鍵。// 支付回調(diào)微信用 XML POST 通知先驗(yàn)簽再改訂單最后必須返回 SUCCESS [Route(api/pay/notify)] public string Notify() { var xml ReadRawBody(); // 拿到微信 POST 過來的原始 XML if (!WxPaySign.Verify(xml, _merchantKey)) return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[sign error]]/return_msg/xml; var result WxPayResult.FromXml(xml); var order _orderRepo.GetByNo(result.OutTradeNo); if (order null) return FailXml(order not found); // 冪等處理已經(jīng)支付過的訂單直接返回 SUCCESS防止重復(fù)通知重復(fù)入賬 if (order.Status OrderStatus.Paid) return SuccessXml(); // 金額以分為單位必須和訂單金額比對防篡改 if (order.AmountInFen ! result.TotalFee) return FailXml(amount mismatch); _orderRepo.MarkPaid(order.Id, result.TransactionId); return SuccessXml(); }回調(diào)里有三個必須守住的規(guī)矩第一驗(yàn)簽不通過直接返回 FAIL讓微信重試但千萬別在日志里打印完整密鑰第二金額必須是整數(shù)分而且和訂單表逐位比對否則會被構(gòu)造回調(diào)刷訂單狀態(tài)第三冪等判斷放在改狀態(tài)之前微信的支付通知會重試 25 次不冪等就會重復(fù)發(fā)貨或重復(fù)加余額。另外回調(diào)地址必須是外網(wǎng) HTTPS 能訪問到的 URL本地調(diào)試可以先用內(nèi)網(wǎng)穿透工具把端口映射出去再把回調(diào)地址配成穿透域名。5. 避坑Session 失效、腳本中斷、支付不回調(diào)與并發(fā)扣庫存這一章是我在這類商城源碼上反復(fù)踩過的坑全部按現(xiàn)象 → 原因 → 解決來寫每一條都值得保存到自己的部署筆記里。這些坑的共同特點(diǎn)是不報(bào)編譯錯誤不在日志里留明顯痕跡但會在線上悄悄咬你一口等用戶投訴了才暴露。5.1 小程序請求一會兒登錄失效一會兒正常Cookie 與 Session 的錯位現(xiàn)象小程序端登錄后能看幾分鐘商品點(diǎn)進(jìn)詳情頁或加購物車就報(bào)請先登錄刷新又偶爾正常。原因老源碼的登錄態(tài)存在 ASP.NET Session 里靠瀏覽器 Cookie 維系小程序wx.request不維護(hù) Cookie服務(wù)端只能靠前端手動帶上來的會話標(biāo)識認(rèn)人。如果前端只在登錄時把 sessionId 塞進(jìn) Storage而后端 Session 有滑動過期時間操作一慢就過期表現(xiàn)就是時好時壞非常玄學(xué)。解決把接口鑒權(quán)從 Session 改成 token或者在前端請求封裝里手動維護(hù)ASP.NET_SessionId。我一般直接改 token登錄接口返回 token寫進(jìn)utils/request.js的 header后端用過濾器統(tǒng)一校驗(yàn)。改動成本大約半天換來的是不再被 Session 過期折騰。5.2 SQL 腳本執(zhí)行到一半中斷版本、編碼與批次問題現(xiàn)象在 SSMS 里打開完整 .sql點(diǎn)執(zhí)行跑了一百多行后報(bào)錯停止前面建的表沒回滾后面建的表沒了。原因一是腳本里混著老版本 SQL Server 語法新實(shí)例兼容但某些系統(tǒng)存儲過程行為變了二是腳本文件是 GBK 編碼而 SSMS 按 UTF-8 讀中文注釋或數(shù)據(jù)變成亂碼導(dǎo)致語法錯誤三是文件里包含 GO 批處理指令放在程序里按整段執(zhí)行就會報(bào)錯。解決按文件拆分執(zhí)行先建庫建表再插數(shù)據(jù)最后跑存儲過程執(zhí)行前統(tǒng)一把腳本另存為 UTF-8 with BOM這一步能根治亂碼如果是交付給客戶我一般會生成一份一鍵導(dǎo)入小工具用 sqlcmd 按文件順序執(zhí)行并輸出日志哪一步失敗一眼就能看到。5.3 支付成功但訂單還是未付款回調(diào)地址與 SUCCESS 返回現(xiàn)象用戶微信里扣了錢商城后臺訂單狀態(tài)還是待付款用戶反復(fù)投訴開發(fā)者查日志發(fā)現(xiàn)支付回調(diào)根本沒到。原因回調(diào)地址配置成了 http://localhost:5000/api/pay/notify微信服務(wù)器當(dāng)然訪問不到或者回調(diào)接口收到通知后處理異常拋了 500沒有按協(xié)議返回 SUCCESS微信會重試 25 次時間長了就放棄。解決先確認(rèn)你在微信商戶平臺配置的 API 回調(diào)地址是公網(wǎng)可訪問的 HTTPS 地址再在回調(diào)入口第一行寫日志把原始 XML 落盤沒日志一切排查都是猜。本地聯(lián)調(diào)用內(nèi)網(wǎng)穿透映射到本地端口再把回調(diào) URL 配成穿透域名驗(yàn)證通過后再切正式域名。5.4 并發(fā)下單庫存變負(fù)數(shù)條件更新與行鎖現(xiàn)象活動預(yù)熱時用戶瘋狂搶運(yùn)營后臺的庫存變成 -12還有不少訂單付款后沒法發(fā)貨。原因扣庫存代碼通常是三段式查出庫存 → 判斷大于 0 → 減一。兩個請求同時通過判斷后寫覆蓋先寫庫存就穿底。商城源碼里這種寫法非常普遍屬于最典型的并發(fā)翻車不是線上壓測根本暴露不出來。解決扣庫存的 SQL 必須寫成條件更新影響行數(shù)為 0 就說明庫存不足配合事務(wù)把扣庫存和創(chuàng)建訂單放同一個事務(wù)里訂單失敗就回滾-- 條件更新stock 要扣的數(shù)量才扣返回影響行數(shù)判斷成敗 UPDATE dbo.Goods SET Stock Stock - count WHERE Id goodsId AND Stock count;普通商城用上面的條件更新已經(jīng)足夠活動場景再把扣減動作放到 Redis 的 DECR 上做預(yù)扣但那是另一個復(fù)雜度層級別一上來就上容易把自己繞進(jìn)去。5.5 API 返回 /Date(1600000000000)/ 時間格式小程序端解析崩潰現(xiàn)象商品詳情頁的時間字段顯示成一串/Date(1600000000000)/訂單列表按時間排序全亂。原因老項(xiàng)目用 Newtonsoft.Json 的默認(rèn) DateTime 序列化輸出的是這種微軟私有的 Date 格式小程序端new Date(str)解析不了黑匣子一樣看不出原因。解決在 asp.net core 項(xiàng)目里配置 JSON 序列化時換成 ISO 格式// 統(tǒng)一時間格式為 ISO 8601小程序端 new Date(2024-01-01T10:00:00) 可直接解析 services.AddControllers() .AddNewtonsoftJson(opts opts.SerializerSettings.DateFormatHandling DateFormatHandling.IsoDateFormat );順手把時區(qū)也統(tǒng)一了數(shù)據(jù)庫存 UTC接口層轉(zhuǎn)北京時間輸出否則不同服務(wù)器的時區(qū)會導(dǎo)致訂單時間對不上。這個問題在 Framework 老項(xiàng)目里更隱蔽因?yàn)樗辉?JSON 輸出那一層有問題頁面端 Razor 渲染反而正常所以很多人查半天查不到。6. 上線前用一把 curl 腳本做接口回歸和部署自檢發(fā)布不等于部署完。配置好服務(wù)器、小程序端也換了正式域名之后我做的最后一件事是回歸驗(yàn)證拿 curl 把商城核心鏈路按順序打一遍比任何代碼評審都管用。6.1 核心接口逐個打一遍HTTP 200 不等于業(yè)務(wù)成功#!/bin/bash # 上線前接口回歸依次檢查核心接口的 HTTP 狀態(tài)碼與業(yè)務(wù) code BASEhttps://yourmall.com TOKEN$(curl -s -X POST $BASE/api/wx/login -d codetest_code \ | python3 -c import json,sys;print(json.load(sys.stdin)[data][token])) for item in goods/list goods/detail?id1; do res$(curl -s -o /tmp/r.json -w %{http_code} \ -H Authorization: Bearer $TOKEN $BASE/api/$item) code$(python3 -c import json;print(json.load(open(/tmp/r.json))[code]) 2/dev/null) echo $item - http$res code$code done腳本把 HTTP 狀態(tài)碼和業(yè)務(wù) code 分開看HTTP 200 只代表請求送達(dá)code 非 0 才是業(yè)務(wù)層報(bào)錯。登錄里的 test_code 是占位符真實(shí)聯(lián)調(diào)時換成 wx.login 拿到的 code。把訂單創(chuàng)建、支付回調(diào)、訂單查詢也按這個格式加進(jìn)去十幾秒就能把全站主鏈路過一遍發(fā)布后每天早上跑一次接口掛了比你先知道?;貧w通過后我會再做一遍冷啟動自檢換一臺干凈機(jī)器按部署文檔從裝運(yùn)行時、還原數(shù)據(jù)庫、改連接串、配 HTTPS、配小程序合法域名完整走一遍把文檔缺的步驟補(bǔ)進(jìn)去。我吃過最大的虧就是沒寫這臺機(jī)器已經(jīng)裝過什么結(jié)果客戶換臺服務(wù)器直接部署失敗。最后說句個人習(xí)慣每次發(fā)布前支付回調(diào)地址、AppSecret、連接串這三樣?xùn)|西單獨(dú)列一個清單核對一遍。這三個位置出問題都不報(bào)明顯錯誤而是用戶悄悄流失等你發(fā)現(xiàn)已經(jīng)晚了。希望這篇能幫你接手這套 ASP.NET 商城源碼時少繞幾個彎把時間留給真正的業(yè)務(wù)功能。本文還有配套的精品資源點(diǎn)擊獲取