:訂單狀態(tài)機(jī)與擔(dān)保交易實(shí)戰(zhàn))
終于把這個(gè)基于Spring Boot的手辦模玩交易平臺(tái)擼完了。從需求評(píng)審到部署上線折騰了小兩個(gè)月中間踩了不少坑也積累了一些實(shí)打?qū)嵉慕?jīng)驗(yàn)。這篇文章就圍繞這個(gè)項(xiàng)目從需求分析到技術(shù)落地把關(guān)鍵的設(shè)計(jì)思路和實(shí)現(xiàn)細(xì)節(jié)從頭到尾捋一遍希望對(duì)正在做類似垂直電商、或者拿這個(gè)題目做畢設(shè)的朋友有點(diǎn)參考價(jià)值。先說(shuō)一下結(jié)局整個(gè)系統(tǒng)按Spring Boot 2.7.18 MyBatis-Plus Redis Elasticsearch這套主流組合落地支撐了用戶端、賣家端、平臺(tái)管理后臺(tái)三條業(yè)務(wù)線預(yù)售、定金尾款、驗(yàn)貨擔(dān)保這些模玩交易特有的流程都跑通了。1. 手辦模玩交易平臺(tái)與普通電商的本質(zhì)差異1.1 模玩圈的交易現(xiàn)狀與真實(shí)痛點(diǎn)手辦模玩交易最典型的場(chǎng)景是一個(gè)玩家抽到了隱藏款或者購(gòu)買后發(fā)現(xiàn)不想要的款式希望快速出手回血另一個(gè)玩家則愿意出高價(jià)收購(gòu)心儀的限定版。這類交易商品包括手辦、模型、雕像、盲盒、毛絨公仔以及相關(guān)配件每一類的定價(jià)邏輯、保存狀態(tài)和轉(zhuǎn)讓方式都截然不同。最早的交易主要集中在貼吧、閑魚(yú)、QQ群里靠的是人肉置頂和聊天記錄來(lái)確認(rèn)購(gòu)買意向糾紛率奇高。做這個(gè)平臺(tái)之前我專門扒了幾百條圈內(nèi)交易糾紛的帖子發(fā)現(xiàn)核心矛盾集中在信息不對(duì)稱和信任缺失上——買家擔(dān)心錢付了貨不對(duì)板賣家擔(dān)心貨發(fā)了買家惡意退貨。所以平臺(tái)的第一設(shè)計(jì)目標(biāo)不是功能多而是怎么用系統(tǒng)流程消除交易雙方的不確定性。1.2 交易形態(tài)與角色關(guān)系梳理手辦模玩交易的形態(tài)比標(biāo)準(zhǔn)電商復(fù)雜得多。平臺(tái)中核心角色有三個(gè)買家、賣家和平臺(tái)運(yùn)營(yíng)方此外還有參與簽收鑒定的第三方驗(yàn)貨人員。交易形態(tài)上除了常見(jiàn)的現(xiàn)貨直接購(gòu)買外大量存在預(yù)訂交易——玩家在廠商正式出貨前先支付定金或全款等到貨之后再補(bǔ)尾款。這種交易天然帶有很長(zhǎng)的時(shí)間跨度和價(jià)格波動(dòng)風(fēng)險(xiǎn)熱門款式可能漲價(jià)翻倍冷門款式可能跌破預(yù)定價(jià)格。更有意思的是模玩交易中普遍存在全款交易和補(bǔ)款交易兩個(gè)環(huán)節(jié)全款交易指買家一次性完成支付賣家直接發(fā)貨補(bǔ)款交易則指預(yù)定場(chǎng)景中先付定金鎖定名額到貨后再支付尾款并安排發(fā)貨。這兩種交易的時(shí)間節(jié)點(diǎn)完全不同若用同一套普通商品訂單邏輯處理會(huì)出現(xiàn)定金支付后無(wú)法跟蹤尾款、中途取消后退款金額計(jì)算錯(cuò)誤等一連串問(wèn)題。因此系統(tǒng)的數(shù)據(jù)模型必須從一開(kāi)始就區(qū)分現(xiàn)貨訂單和預(yù)售訂單并且在設(shè)計(jì)訂單狀態(tài)機(jī)時(shí)考慮定金、尾款、取消、退款多種組合路徑。1.3 差異化需求決定了系統(tǒng)設(shè)計(jì)方向仔細(xì)拆解這些業(yè)務(wù)場(chǎng)景后我確定了系統(tǒng)設(shè)計(jì)的核心方向。其一每件商品要有獨(dú)立的成色描述體系不能像淘寶那樣統(tǒng)一用全新二手模糊帶過(guò)必須落到具體瑕疵位置、拍照角度。其二必須引入平臺(tái)驗(yàn)貨擔(dān)保機(jī)制賣家發(fā)貨到平臺(tái)平臺(tái)拍照查驗(yàn)后再寄給買家資金在驗(yàn)貨完成前處于凍結(jié)狀態(tài)。其三由于單個(gè)款式存在大量規(guī)格變體——同一款手辦可能有普通版、豪華版、特典版且每個(gè)版本的價(jià)格和庫(kù)存都不同商品模型需要采用標(biāo)準(zhǔn)SPU SKU結(jié)構(gòu)。這套設(shè)計(jì)邏輯直接決定了我后面技術(shù)選型的方向。普通電商系統(tǒng)里做不好模玩交易不是因?yàn)榇a能力問(wèn)題而是業(yè)務(wù)模型沒(méi)設(shè)對(duì)。后面的架構(gòu)和實(shí)現(xiàn)基本都是圍繞這三條差異化需求展開(kāi)的。2. Spring Boot技術(shù)選型與模塊化架構(gòu)落地2.1 為什么是Spring Boot 2.7.18 MyBatis-Plus Redis項(xiàng)目的基礎(chǔ)框架選用Spring Boot 2.7.18原因有三個(gè)。第一2.x版本生態(tài)最成熟無(wú)論是網(wǎng)上資料還是本地開(kāi)發(fā)環(huán)境遇到問(wèn)題都能快速找到解決路徑第二3.x版本在JDK17以上運(yùn)行但當(dāng)時(shí)團(tuán)隊(duì)環(huán)境普遍還是JDK8強(qiáng)行上3.x會(huì)引入適配成本第三系統(tǒng)的核心并發(fā)壓力并不高——手辦模玩交易是小眾垂直市場(chǎng)日活用戶量級(jí)遠(yuǎn)小于大平臺(tái)真正需要對(duì)抗的是超賣、重復(fù)下單、狀態(tài)錯(cuò)亂這幾類業(yè)務(wù)邏輯問(wèn)題而不是性能瓶頸。持久層選了MyBatis-Plus而不是JPA核心判斷依據(jù)是模玩交易系統(tǒng)中查詢條件極為復(fù)雜常用字段組合包括IP系列、材質(zhì)、比例、發(fā)售年份、成色等級(jí)、當(dāng)前狀態(tài)、價(jià)格區(qū)間等十幾個(gè)條件MyBatis靈活的XML編寫(xiě)方式能把這些復(fù)雜SQL寫(xiě)清楚配合分頁(yè)插件和代碼生成器開(kāi)發(fā)效率明顯高于手動(dòng)編寫(xiě)JdbcTemplate。緩存方面使用Redis主要承擔(dān)熱點(diǎn)IP商品詳情緩存、接口限流計(jì)數(shù)器和JWT Token管理三個(gè)任務(wù)。搜索采用的是Elasticsearch將商品數(shù)據(jù)的查詢?nèi)孔呷乃饕唧w實(shí)現(xiàn)思路在第五部分詳細(xì)說(shuō)明。2.2 服務(wù)劃分與模塊邊界項(xiàng)目沒(méi)有用微服務(wù)架構(gòu)不是說(shuō)微服務(wù)不好而是當(dāng)前業(yè)務(wù)階段用Domain模塊化拆分會(huì)更合適避免引入服務(wù)注冊(cè)中心、配置中心、分布式事務(wù)等大量基礎(chǔ)設(shè)施控制老系統(tǒng)維護(hù)成本。工程結(jié)構(gòu)上做的是Maven多模塊拆分各模塊職責(zé)非常清晰acg-common通用工具類、全局異常處理、統(tǒng)一返回體、常量定義acg-web用戶端Web接口面向買家、賣家的標(biāo)準(zhǔn)Rest APIacg-admin管理后臺(tái)接口面向平臺(tái)運(yùn)營(yíng)、審核、驗(yàn)貨人員acg-service業(yè)務(wù)邏輯層各類Service實(shí)現(xiàn)acg-mapperMyBatis的Mapper接口與XML映射文件acg-modelEntity、DTO、VO、枚舉定義這個(gè)方案的好處是保留了單體應(yīng)用的高效迭代和直接部署優(yōu)勢(shì)又通過(guò)模塊邊界強(qiáng)迫開(kāi)發(fā)人員劃分代碼歸屬。實(shí)際開(kāi)發(fā)中經(jīng)常出現(xiàn)不分層、controller里直接寫(xiě)SQL的不良習(xí)慣多模塊劃分至少能在物理層面攔截這類問(wèn)題。2.3 核心數(shù)據(jù)表設(shè)計(jì)與關(guān)系建模數(shù)據(jù)庫(kù)采用MySQL 8.0 InnoDB引擎utf8mb4字符集因?yàn)樯唐窐?biāo)題中包含大量日文漢字和特殊符號(hào)。訂單、商品、用戶這類核心表放在同一個(gè)庫(kù)中管理重要表結(jié)構(gòu)的設(shè)計(jì)思路值得展開(kāi)說(shuō)。用戶表里額外增加了credit_score信譽(yù)分、seller_level賣家等級(jí)、real_name_status實(shí)名認(rèn)證狀態(tài)三個(gè)字段。加這幾個(gè)字段的原因在于模玩交易平臺(tái)需要快速識(shí)別可信賣家通過(guò)信用分和實(shí)名狀態(tài)來(lái)支撐驗(yàn)貨擔(dān)保的審核機(jī)制。商品表是最復(fù)雜的表拆成了product主表和product_sku變更表。主表存IP系列、品牌、官方發(fā)售價(jià)、商品類型、貨號(hào)SKU表存規(guī)格差異、圖片鏈接、成色描述、狀態(tài)預(yù)售場(chǎng)景會(huì)額外存儲(chǔ)一個(gè)pre_sale_infoJSON字段里面包含定金金額、尾款截止時(shí)間、預(yù)計(jì)到貨日期。用JSON而不是硬編碼獨(dú)立表是因?yàn)轭A(yù)售參數(shù)在不同商品間差異很大用獨(dú)立字段會(huì)大量出現(xiàn)NULL空值JSON反而更靈活。訂單表則用order_type字段區(qū)分現(xiàn)貨訂單和預(yù)售補(bǔ)款訂單同時(shí)用多個(gè)關(guān)聯(lián)字段分別記錄定金支付時(shí)間和尾款支付時(shí)間。order_status這一列是整個(gè)交易鏈路的核心通過(guò)枚舉嚴(yán)格控制狀態(tài)流轉(zhuǎn)。訂單表和支付流水表payment_record是分開(kāi)的每筆資金變動(dòng)都獨(dú)立落表方便對(duì)賬。MySQL索引設(shè)計(jì)遵循高頻查詢優(yōu)先原則商品表以(status, category_id)開(kāi)頭的聯(lián)合索引保證商城主頁(yè)的商品列表效率訂單表常規(guī)狀態(tài)下單量大、查詢按用戶和時(shí)間維度為主建立(user_id, create_time)聯(lián)合索引支付流水表以order_no為唯一索引。實(shí)際上線跑了兩周數(shù)據(jù)后最慢的查詢也控制在300ms以內(nèi)。3. 商品中心的實(shí)現(xiàn)規(guī)格、成色、驗(yàn)貨報(bào)告一個(gè)都不能少3.1 多規(guī)格SKU與預(yù)售商品的數(shù)據(jù)結(jié)構(gòu)商品中心是整個(gè)平臺(tái)的基石因?yàn)槭洲k模玩SKU的差異點(diǎn)非常細(xì)。一款熱門動(dòng)漫的1/7比例手辦可能同時(shí)有普通版豪華限定版特典版三個(gè)SKU。普通版附基礎(chǔ)底座豪華版多一個(gè)替換臉型特典版額外帶一個(gè)迷你掛件它們的價(jià)格、庫(kù)存和上架時(shí)間都不一樣。若按傳統(tǒng)電商一個(gè)商品一個(gè)詳情頁(yè)、一堆簡(jiǎn)單規(guī)格參數(shù)的模式存儲(chǔ)就無(wú)法體現(xiàn)差異用戶在下單參數(shù)選擇上也易產(chǎn)生歧義。這里我采用了SPU SKU雙層結(jié)構(gòu)存儲(chǔ)SPU層保存共用的主體圖片、IP系列、比例、廠家信息和詳情描述SKU層保存規(guī)格名稱如豪華限定版、單獨(dú)價(jià)格、單獨(dú)庫(kù)存和規(guī)格專屬圖片。因?yàn)槟M嫔唐稴KU數(shù)量通常不多所以商品詳情接口一次查詢會(huì)帶出SPU數(shù)據(jù)、SKU列表、全部圖片和相關(guān)標(biāo)簽前端做合并展示即可。預(yù)售商品在SKU表中用product_type2標(biāo)識(shí)同時(shí)在pre_sale_info字段中保存定金信息。用戶下單時(shí)會(huì)明確標(biāo)記訂單類型將預(yù)售價(jià) 定金 補(bǔ)款拆分定金支付成功后形成預(yù)占狀態(tài)到貨后系統(tǒng)自動(dòng)通知用戶補(bǔ)款這一整個(gè)業(yè)務(wù)流程走的是單獨(dú)一套狀態(tài)機(jī)。3.2 成色分級(jí)標(biāo)準(zhǔn)與描述模板很多做二手交易平臺(tái)的產(chǎn)品會(huì)忽略一個(gè)問(wèn)題成色標(biāo)準(zhǔn)不統(tǒng)一是糾紛的第一大來(lái)源。交易模玩時(shí)賣家說(shuō)幾乎全新買家拿到手后發(fā)現(xiàn)盒子上有一道劃痕就認(rèn)定貨不對(duì)板。平臺(tái)從一開(kāi)始就要在平臺(tái)上建立統(tǒng)一的成色分級(jí)規(guī)范。參考主流圈內(nèi)共識(shí)和閑魚(yú)實(shí)踐我設(shè)計(jì)了96新-99新-全新未拆-拆擺微瑕-有明顯瑕疵五檔體系每個(gè)檔次對(duì)應(yīng)詳細(xì)描述模板賣家上架時(shí)必須按模板選擇。全新未拆要求原盒包裝完整、塑封完好99新指整體保存極佳僅有輕微存放痕跡96新允許存在可拍攝到的細(xì)微瑕疵有明顯瑕疵則必須具體說(shuō)明瑕疵類型。在商品發(fā)布表單中賣家需要逐條填寫(xiě)瑕疵位置、程度和實(shí)拍圖片不填就無(wú)法提交審核。這套機(jī)制上線后在糾紛處理上產(chǎn)生了明顯的正向價(jià)值??头槿霠?zhēng)議時(shí)只需對(duì)照成色描述和驗(yàn)貨照片就能快速判斷責(zé)任歸屬而不是靠買賣雙方各執(zhí)一詞、反復(fù)掰扯。3.3 圖片存儲(chǔ)與驗(yàn)貨報(bào)告生成模玩類商品不像數(shù)碼產(chǎn)品那樣用幾張標(biāo)準(zhǔn)圖就能說(shuō)清狀態(tài)賣家如果只上傳三張圖基本無(wú)法判斷瑕疵和保存情況??紤]到這一點(diǎn)系統(tǒng)把圖片上傳數(shù)量放寬到最多15張并強(qiáng)制要求包含正面、背面、左側(cè)、右側(cè)、底部、瑕疵特寫(xiě)六類視圖。上傳組件用分片直傳方式對(duì)接MinIO對(duì)象存儲(chǔ)同時(shí)通過(guò)Spring Boot自定義注解對(duì)圖片文件做格式只允許jpg/png/webp和大小單張限5MB雙重校驗(yàn)。在驗(yàn)貨擔(dān)保流程中平臺(tái)工作人員收貨后需要拍攝驗(yàn)貨照片并逐張?zhí)顚?xiě)驗(yàn)貨結(jié)論最終自動(dòng)生成一份帶水印和二維碼的驗(yàn)貨報(bào)告PDF。這里踩過(guò)一個(gè)比較典型的坑——PDF中文文字渲染成方塊后來(lái)通過(guò)引入開(kāi)源字體庫(kù)并設(shè)置FontProperties才解決。驗(yàn)貨報(bào)告生成后保存到MinIO并關(guān)聯(lián)訂單買家驗(yàn)貨通過(guò)后才能在后臺(tái)觸發(fā)確認(rèn)收貨操作。3.4 商品發(fā)布審核鏈路商品發(fā)布不是直接上架而是先進(jìn)入待審核狀態(tài)。管理后臺(tái)的審核人員查看商品信息、圖片、成色描述和賣家資歷后執(zhí)行通過(guò)或駁回。駁回理由必須明確具體系統(tǒng)會(huì)將駁回原因以站內(nèi)信方式通知賣家。為了保證審核不積壓我在管理后臺(tái)增加了自動(dòng)提醒功能——待審核商品超過(guò)24小時(shí)未處理時(shí)系統(tǒng)自動(dòng)給審核人員推送提醒。這條鏈路讓商品質(zhì)量和合規(guī)性有了基本保障。4. 交易鏈路的狀態(tài)機(jī)設(shè)計(jì)與擔(dān)保交易邏輯4.1 現(xiàn)貨訂單與預(yù)售訂單的狀態(tài)設(shè)計(jì)交易模塊是整個(gè)平臺(tái)最需要小心處理的部分我用顯式的狀態(tài)機(jī)來(lái)管理訂單而不是散落在addOrder、updateOrder里手動(dòng)if/else?,F(xiàn)貨訂單的狀態(tài)定義為public enum OrderStatus { PENDING_PAYMENT(0, 待付款), PAID(1, 已付款待發(fā)貨), SHIPPED(2, 已發(fā)貨待收貨), VERIFYING(3, 驗(yàn)貨中), COMPLETED(4, 已完成), CANCELLED(5, 已取消), REFUNDING(6, 退款中), REFUNDED(7, 已退款), DISPUTED(8, 爭(zhēng)議中); private final int code; private final String desc; }每個(gè)狀態(tài)變更都在Service方法中做合法校驗(yàn)不允許跨狀態(tài)跳轉(zhuǎn)。比如已付款的訂單可以直接進(jìn)入待發(fā)貨但待付款訂單不能直接跳到驗(yàn)貨中因?yàn)槿鄙侔l(fā)貨物流信息。新增狀態(tài)時(shí)只需要在枚舉中增加一項(xiàng)并在狀態(tài)轉(zhuǎn)換表中補(bǔ)充允許路徑維護(hù)起來(lái)非常直觀。預(yù)售訂單的狀態(tài)則多出定金待支付和待補(bǔ)尾款兩個(gè)環(huán)節(jié)實(shí)際處理時(shí)我用一個(gè)order_stage字段來(lái)標(biāo)記定金階段/尾款階段/已完成階段使得兩個(gè)階段共用同一張訂單表而不會(huì)混淆。4.2 擔(dān)保交易與驗(yàn)貨環(huán)節(jié)的錢貨分離手辦模玩圈子頻繁出現(xiàn)的一個(gè)糾紛是買家轉(zhuǎn)賬之后賣家直接拉黑。為了解決這個(gè)問(wèn)題平臺(tái)引入了擔(dān)保交易模式買家支付的錢先進(jìn)入平臺(tái)凍結(jié)賬戶并不直接打給賣家。賣家看到買家已付款后發(fā)貨到平臺(tái)驗(yàn)貨點(diǎn)驗(yàn)貨人員確認(rèn)商品與描述一致后再轉(zhuǎn)發(fā)給買家買家確認(rèn)收貨后平臺(tái)才將款項(xiàng)解凍并結(jié)算給賣家。這個(gè)過(guò)程在實(shí)現(xiàn)上關(guān)鍵是凍結(jié)資金的狀態(tài)是綁定在訂單上的通過(guò)支付流水記錄的frozen_status字段控制而不是簡(jiǎn)單地把錢放在一個(gè)余額字段里。驗(yàn)貨環(huán)節(jié)我設(shè)計(jì)了一張verification_record表記錄驗(yàn)貨人員、驗(yàn)貨時(shí)間、驗(yàn)貨照片、驗(yàn)貨結(jié)論和備注每次驗(yàn)貨結(jié)果都會(huì)觸發(fā)訂單狀態(tài)變更。這套擔(dān)保機(jī)制的代碼邏輯并不復(fù)雜但它需要業(yè)務(wù)上嚴(yán)格執(zhí)行未驗(yàn)貨不放款的規(guī)則技術(shù)只是把規(guī)則落實(shí)了。4.3 退款與爭(zhēng)議流程的關(guān)鍵實(shí)現(xiàn)退款流程要處理的情況比較多沒(méi)發(fā)貨前買家申請(qǐng)退款、驗(yàn)貨不合格自動(dòng)觸發(fā)退款、確認(rèn)收貨后又發(fā)現(xiàn)貨不對(duì)板申請(qǐng)售后。我按不同節(jié)點(diǎn)做了不同處理邏輯。買家申請(qǐng)退款時(shí)系統(tǒng)首先檢查訂單狀態(tài)和付款時(shí)間如果訂單處于已付款待發(fā)貨狀態(tài)則平臺(tái)自動(dòng)同意退款并原路退回金額如果訂單已發(fā)貨或驗(yàn)貨中則需要平臺(tái)客服介入審核。爭(zhēng)議處理環(huán)節(jié)設(shè)計(jì)了人工客服后臺(tái)客服可以查看到完整的訂單時(shí)間線、物流信息、驗(yàn)貨報(bào)告和聊天記錄。由于狀態(tài)機(jī)設(shè)計(jì)得足夠清晰客服操作其實(shí)只是在幾個(gè)合法狀態(tài)間做選擇。這里項(xiàng)目中使用了Spring Boot的Transactional來(lái)保證退款金額變更和訂單狀態(tài)更新的事務(wù)一致性——尤其注意轉(zhuǎn)賬和狀態(tài)更新必須在一個(gè)事務(wù)里否則會(huì)出現(xiàn)錢退了但訂單還是已付款的臟數(shù)據(jù)。這個(gè)細(xì)節(jié)看起來(lái)簡(jiǎn)單但線上排查問(wèn)題過(guò)程中經(jīng)常會(huì)發(fā)現(xiàn)類似這種相差一個(gè)事務(wù)注解的bug。5. 搜索、標(biāo)簽與反欺詐的工程化落地5.1 多維標(biāo)簽體系與Elasticsearch集成手辦模玩平臺(tái)里面用戶購(gòu)買行為通常是目標(biāo)導(dǎo)向型——找某個(gè)IP、某個(gè)比例、某個(gè)廠家的特定手辦很少像逛淘寶一樣漫無(wú)目的瀏覽。因此搜素體驗(yàn)成為平臺(tái)核心競(jìng)爭(zhēng)力。系統(tǒng)設(shè)計(jì)了一套多維標(biāo)簽體系標(biāo)簽類型包括IP系列、角色名比例1/7、1/8等品牌/廠商材質(zhì)PVC、樹(shù)脂、ABS發(fā)售年份版本類型以及成色等級(jí)。每個(gè)標(biāo)簽在商品上架時(shí)由賣家選擇管理后臺(tái)可以維護(hù)標(biāo)簽庫(kù)。商品數(shù)據(jù)寫(xiě)入時(shí)通過(guò)Spring Boot定時(shí)任務(wù)同步到Elasticsearch索引結(jié)構(gòu)按商品維度建模索引字段全部采用keyword類型以便精確過(guò)濾。查詢時(shí)基于seller_type、scale等字段組合過(guò)濾基于match做關(guān)鍵詞搜索。初期數(shù)據(jù)量不大ES單節(jié)點(diǎn)足以支撐查詢需求。若后續(xù)數(shù)據(jù)量增長(zhǎng)只需要將ES升級(jí)為集群并增加分片副本數(shù)即可。5.2 JWT認(rèn)證與接口冪等設(shè)計(jì)為防止訂單重復(fù)提交和接口被刷交易相關(guān)接口做了冪等控制。每次下單請(qǐng)求前端會(huì)生成一個(gè)Idempotent-Token后端用Redis的setIfAbsent做標(biāo)識(shí)去重。請(qǐng)求進(jìn)來(lái)時(shí)攔截器會(huì)檢查Token是否存在如果沒(méi)有則正常放行并將Token寫(xiě)入Redis設(shè)置對(duì)應(yīng)訂單關(guān)閉時(shí)間的過(guò)期時(shí)間如果Token已存在直接返回重復(fù)請(qǐng)求。用戶認(rèn)證方案選擇了比較輕量的JWT配合Redis使用。登錄成功后發(fā)放Token并把userId寫(xiě)入Redis有效期為7天每次請(qǐng)求攔截器解析Token查詢Redis確認(rèn)有效。用戶退出時(shí)主動(dòng)刪除Redis中的Token實(shí)現(xiàn)失效。加上Spring Security做接口級(jí)權(quán)限控制將用戶、賣家、管理員的角色權(quán)限從URL級(jí)別隔離代碼結(jié)構(gòu)很清晰后續(xù)也方便擴(kuò)展。5.3 文件上傳與全局XSS過(guò)濾與多數(shù)Web項(xiàng)目一樣系統(tǒng)是存在XSS風(fēng)險(xiǎn)的特別是手辦模玩平臺(tái)中商品描述和社區(qū)評(píng)論都允許用戶輸入富文本內(nèi)容如果不過(guò)濾惡意腳本輕則指數(shù)彈窗重則賬號(hào)劫持。我實(shí)現(xiàn)了一個(gè)全局過(guò)濾器對(duì)請(qǐng)求參數(shù)中的HTML標(biāo)簽和JavaScript腳本關(guān)鍵字做轉(zhuǎn)義處理尤其重點(diǎn)關(guān)注了用戶上傳PDF這類文件的實(shí)際場(chǎng)景——PDF文件中也能嵌入惡意腳本或外鏈若用戶把PDF當(dāng)作商品圖片上傳可能會(huì)形成間接的XSS入口??紤]到項(xiàng)目實(shí)際需求滲透測(cè)試和日常監(jiān)控中發(fā)現(xiàn)直接限制文件類型、校驗(yàn)文件簽名、全局對(duì)參數(shù)做白名單過(guò)濾是最直接有效的方法。上傳接口精心設(shè)計(jì)后將文件類型白名單、大小限制、內(nèi)容校驗(yàn)都配置好系統(tǒng)實(shí)際運(yùn)行時(shí)大幅減少了惡意文件上傳事件。6. 部署上線前的優(yōu)化清單與踩坑記錄6.1 項(xiàng)目打包與運(yùn)行環(huán)境配置項(xiàng)目采用Maven多模塊構(gòu)建最終通過(guò)spring-boot-maven-plugin打jar包。打包時(shí)遇到一個(gè)很典型的問(wèn)題子模塊之間的依賴沒(méi)有正確打包進(jìn)去導(dǎo)致jar包運(yùn)行時(shí)拋出ClassNotFound異常。再三排查后發(fā)現(xiàn)是因?yàn)镾pring Boot插件配置在子模塊上將插件配置移到啟動(dòng)模塊后問(wèn)題才解決。生產(chǎn)環(huán)境我使用Docker方式部署Dockerfile采用分階段構(gòu)建先用Maven鏡像編譯再用JRE鏡像運(yùn)行基礎(chǔ)鏡像只保留運(yùn)行所需的最小化JRE環(huán)境鏡像大小從700MB下降到260MB左右。6.2 數(shù)據(jù)庫(kù)連接池與分頁(yè)查詢優(yōu)化上線初期數(shù)據(jù)庫(kù)連接池采用默認(rèn)配置結(jié)果在商品刷新活動(dòng)期間頻繁出現(xiàn)數(shù)據(jù)庫(kù)連接超時(shí)告警。排查下來(lái)發(fā)現(xiàn)Druid連接池初始化和最大連接數(shù)設(shè)置不合理后期調(diào)整為初始10、最小5、最大50并設(shè)置空閑連接檢測(cè)周期問(wèn)題就明顯緩解了。商品列表查詢時(shí)原本使用嵌套子查詢實(shí)現(xiàn)多條件過(guò)濾句法復(fù)雜MySQL優(yōu)化器在部分組合下執(zhí)行計(jì)劃不理想。將SQL改為多表JOIN并配合強(qiáng)制索引核心接口從1500ms以上降到200ms左右。分頁(yè)插件上針對(duì)count優(yōu)化做了特別注意避免大偏移量深翻頁(yè)造成的性能問(wèn)題而是采用基于游標(biāo)的分頁(yè)方式給前端返回每次查詢結(jié)果的最后一個(gè)商品的ID作為下一次查詢的起始位置實(shí)踐效果很好。6.3 其他值得記錄的低頻坑時(shí)區(qū)問(wèn)題數(shù)據(jù)庫(kù)連接URL中不顯式指定serverTimezone會(huì)導(dǎo)致數(shù)據(jù)庫(kù)時(shí)間和本地時(shí)間相差8小時(shí)。所有環(huán)境統(tǒng)一使用Asia/Shanghai時(shí)區(qū)并在JDBC連接串中顯式標(biāo)注。JSON序列化被循環(huán)引用商品詳情接口返回的數(shù)據(jù)結(jié)構(gòu)里商品含SKU列表、SKU又回指商品JPA如果默認(rèn)序列化就會(huì)出現(xiàn)無(wú)限遞歸。在序列化配置里增加了JsonIgnoreProperties注解規(guī)避。Redis序列化方式不統(tǒng)一不同模塊有人用StringRedisTemplate、有人用RedisTemplate導(dǎo)致同一個(gè)key存儲(chǔ)的數(shù)據(jù)使用出現(xiàn)ClassCastException。后來(lái)統(tǒng)一了序列化配置使用GenericJackson2JsonRedisSerializer作為value序列化器。刪除數(shù)據(jù)要盡量假刪除用戶申請(qǐng)刪除商品或下架商品不建議物理刪除記錄因?yàn)闅v史訂單、驗(yàn)貨報(bào)告、交易快照都綁定了商品ID物理刪除一切關(guān)聯(lián)記錄會(huì)給對(duì)賬和糾紛處理留下大麻煩。所有核心表增加deleted邏輯刪除字段。6.4 JVM與內(nèi)存調(diào)優(yōu)服務(wù)運(yùn)行初期經(jīng)常出現(xiàn)Young GC頻繁觀察后發(fā)現(xiàn)堆內(nèi)存分配偏小導(dǎo)致Eden區(qū)頻繁飽和。隨后調(diào)整了JVM啟動(dòng)參數(shù)設(shè)為-Xms512m -Xmx1024m采用G1垃圾回收器并在Dockerfile中通過(guò)JAVA_OPTS注入。調(diào)整后在并發(fā)高峰期老年代增長(zhǎng)平緩Full GC次數(shù)顯著下降。需要對(duì)JVM參數(shù)做驗(yàn)證時(shí)建議直接在本地用相同啟動(dòng)參數(shù)壓測(cè)一輪避免線上盲調(diào)。7. 一些小經(jīng)驗(yàn)與建議系統(tǒng)做完之后我自己最大的感受是這種垂直領(lǐng)域的交易平臺(tái)業(yè)務(wù)規(guī)則理解比代碼能力更稀缺。同樣是電商模玩交易一群面臨的問(wèn)題完全不一樣做一個(gè)手辦模玩交易平臺(tái)重點(diǎn)不是把Spring Boot的技術(shù)組件堆上去——而是真正搞清楚這個(gè)圈子的人怎么交易、什么方式讓他們覺(jué)得靠譜再把業(yè)務(wù)規(guī)則轉(zhuǎn)成數(shù)據(jù)模型和狀態(tài)流轉(zhuǎn)。如果說(shuō)要給準(zhǔn)備做類似項(xiàng)目無(wú)論是畢設(shè)、練手還是真實(shí)產(chǎn)品的人幾個(gè)具體的建議我認(rèn)為比較有價(jià)值的收獲是花時(shí)間認(rèn)真設(shè)計(jì)訂單狀態(tài)機(jī)把每個(gè)可能的用戶路徑下單、付款、發(fā)貨、取消、退款、驗(yàn)貨畫(huà)成狀態(tài)轉(zhuǎn)換圖再開(kāi)始寫(xiě)代碼后面能少改一大半邏輯。把文件上傳和富文本安全當(dāng)成一個(gè)正經(jīng)的安全性防御需求來(lái)處理不要覺(jué)得一個(gè)交易平臺(tái)而已沒(méi)人會(huì)惡意上傳。商品標(biāo)題、商品描述、驗(yàn)貨備注、社區(qū)評(píng)論凡是有用戶輸入的地方全部做統(tǒng)一處理和攔截。如果有條件盡量在release前安排一輪多人使用的內(nèi)測(cè)讓幾個(gè)賣家真實(shí)走一遍發(fā)商品、接單、發(fā)貨的流程會(huì)發(fā)現(xiàn)很多測(cè)試腳本覆蓋不到的操作認(rèn)知問(wèn)題。最后一定用application.yml區(qū)分好開(kāi)發(fā)、測(cè)試、生產(chǎn)環(huán)境的配置私密配置不要提交到代碼倉(cāng)庫(kù)。如果你也在用Spring Boot做類似的交易平臺(tái)希望以上的思路和踩坑記錄能幫上忙。項(xiàng)目本身還遠(yuǎn)稱不上完美但至少上線后已經(jīng)穩(wěn)定跑過(guò)了幾輪小型促銷活動(dòng)沒(méi)有出現(xiàn)超賣、重復(fù)支付或訂單狀態(tài)錯(cuò)亂這類致命問(wèn)題。后續(xù)如果繼續(xù)迭代我可能會(huì)考慮把社區(qū)討論和商品交易進(jìn)一步融合也會(huì)嘗試接入真實(shí)的第三方支付渠道和快遞物流跟蹤讓擔(dān)保交易鏈路更加完整和成熟。