
做个人网上书店那会儿我最初的预期是两周内把前后端跑通交付。真正开始设计之后才发现卖书这件事远比想象中麻烦同一本书不同版次要不要分开上架打折之后怎么保证订单里留的是成交价用户下了单迟迟不付款库存什么时候释放这些细节不拆清楚后面写代码全是雷。这篇文章想把个人网上书店从需求梳理到上线部署的完整路径讲一遍已经做过一遍的人可以看看哪些坑你也踩过准备动手的人可以直接照着搭。我不堆概念只讲实际跑过之后觉得重要、但很多人第一次做会忽略的地方。1. 从一本书到一套系统网上书店的需求梳理1.1 先想清楚做给谁用很多人在网上书店这个项目上栽跟头不是因为代码写不出来而是需求边界没划清楚就开始建表。个人网上书店和其他电商项目最大的不同在于使用者只有两类一类是普通买家一类是自己管理员不存在复杂的运营角色和多级权限体系。把这个前提定下来后面很多决策都会变简单。买家的操作路径大概是注册登录、浏览分类或搜索、查看书籍详情、加入购物车、下单付款、等待收货、确认收货。管理员的路径更短添加图书、维护库存、处理订单发货、取消、看一点基础统计。我当时把这些路径用文字列在文档里没有画多复杂的流程图但每一段路径都对应着后续要建的数据表和接口理清楚了再动工效率明显高很多。如果你最终要输出一份系统设计文档或者论文这部分内容其实就是需求分析章节的核心素材直接能用不需要重新整理。1.2 图书数据比普通商品多了一组出版属性网上书店卖的是书不是普通商品。书有作者、译者、出版社、出版年份、版次、页数、装帧、ISBN这些字段在服装店、数码店的商品表里根本不存在。第一次设计表的时候我差点把图书当成普通商品处理后来发现分类、搜索、详情页展示全都绕不开出版属性。我的建议是把一本书作为最小商品单位不需要像大平台那样搞SPU和SKU的复杂层级。个人书店的量级下一本书就是一个SKU。但要注意一个问题同一本书不同版次、不同装帧精装、平装是否分开上架我在项目里采用了分开上架的方式因为不同版本价格、库存都不同合在一起反而让下单逻辑变得混乱。另外一个容易忽略的字段是上架状态。图书会有下架的时候比如库存为零、或者想临时隐藏某本书。状态字段在后台管理里很常见但很多个人项目把它漏掉了导致想下架一本已经没有货的书只能去删记录非常别扭。1.3 订单流程简化到什么程度合适个人书店没有必要照搬大平台的订单体系。我在设计时果断砍掉了优惠券、复杂售后、多包裹发货这些重功能订单状态只保留五档待付款、已付款、已发货、已完成、已取消。砍功能不等于状态可以随便跳。状态机的流转必须明确比如待付款才能取消已付款才能发货发货之后不能直接跳回待付款。我在后台处理订单状态时每次变更都记录操作日志这样就算哪天误操作改错了状态也能通过日志查出来是谁、什么时候、把订单从什么状态改成了什么状态。这里多说一句状态机设计得越清楚后续写订单接口、写定时任务就越省心。很多网上书店项目做到后面出现订单不知道去哪了的诡异问题十有八九是状态流转没有约束谁来都能改改完也不留痕。2. 图书、库存、订单三张表的字段取舍与坑点2.1 图书主表ISBN不能当主键价格要拆成两个字段图书表是整个系统的基础字段设计直接影响后面所有功能。我实际建表用的字段整理成一张表供参考字段类型说明id自增整数主键title字符串书名author字符串作者translator字符串译者没有则空publisher字符串出版社publish_date日期出版日期isbn字符串唯一索引不做主键category_id整数分类外键price小数定价sale_price小数当前售价cover_url字符串封面图地址description长文本内容简介status布尔/整数上架状态created_at / updated_at时间记录创建与更新时间两个最容易踩的坑我分别说明。第一个坑是ISBN能不能直接当主键。理论上ISBN唯一但实际操作中你会发现同一本书有不同的版次ISBN也不同部分自印书、老版书根本没有ISBN还有个别数据录入时ISBN填错的情况。把ISBN当主键后期改数据会非常痛苦。正确做法是用自增id做主键ISBN加唯一索引既保证了查询效率又留了修改余地。第二个坑是价格字段。第一次做的时候我只放了一个price字段后来想搞限时折扣发现如果直接在price上改订单里的历史价格会跟着变对账对不上。正确的做法是拆成price定价和sale_price当前售价两个字段。前端展示两个价格的对比下单时取sale_price订单明细里再存一份快照这样无论后续怎么调整售价历史订单都不会受影响。2.2 库存表别把可售和锁定混在一个数字里库存设计是整个网上书店项目里最容易被低估的一环。很多demo只在图书表里放一个stock字段下单时直接减一看似没问题实际跑起来会遇到一个很现实的场景用户下单之后不付款这单货到底算不算卖出去了如果算库存被白白占着如果不算那个用户可能永远不付款。我在项目里把库存拆成了三个数字available可售库存、locked锁定库存、sold累计已售。下单成功但未付款时从available减一、加一进locked订单取消或超时关闭时从locked里释放回available只有支付成功才把locked减一、加一进sold。这个设计对应到实际场景是这样的一本书可售3本有个人下单锁了1本但没付款这时候再有两个人下单各自锁1本库存显示可售变成0。第三个人看到页面显示暂时缺货不会出现拍下却发不了货的情况。如果只用一个stock字段无法区分还没卖出去和已经被人锁定但未付款这两种完全不同的状态一遇到用户不付款的订单整个库存数字都是假的。2.3 订单表与订单项为什么订单项要存商品快照订单表本身没什么悬念字段基本就是订单号、用户、订单金额、订单状态、收货人信息、创建时间、支付时间、发货时间、完成时间。真正要留意的是订单明细表order_items。订单项表里至少要有这些字段关联的订单id、图书id、书名快照、封面快照、单价快照、购买数量。这里的快照是指在生成订单的那一刻把图书当前的书名、价格、封面复制一份存进订单项里。为什么必须这么做试想一下某本书标题写错了你后来去改了书名或者一本书价格从60调到了45。如果没有快照历史订单看起来就像当时是以45元卖的、书名还是错的后续对账、售后、统计全都会出问题。我见过有人为了省事订单项里只存book_id查询时再关联图书表结果改过一次书名之后所有历史订单的显示都跟着变了那才叫一个头疼。订单号我也没有直接用数据库自增id。自增id太容易让人看出系统每天的订单量而且很容易被枚举。我用的规则是年月日时分秒4位随机数再配合唯一索引兜底。量级不大碰撞概率很低体验也不错。3. 下单链路的核心扣库存与生成订单的顺序问题3.1 先看超卖是怎么发生的网上书店项目里下单接口是整个系统的核心而核心中的核心是扣库存。很多第一次做的人会写成这样先查一下库存够不够if够再执行库存减一然后生成订单。这个逻辑在只有一个用户测试的时候永远是对的但只要两个用户同时下单同一本书问题就来了。两个请求同时执行SELECT都查到库存还剩下1本都通过了判断然后各自执行UPDATE库存减一数据库里的stock直接变成-1订单却生成了两笔。这就是典型的高并发下的超卖问题。个人书店流量虽然不大但用户同时抢一本热销书是完全可能发生的这个问题不能侥幸。我尝试复现这个场景的时候开了两个终端同时请求同一个接口第一次就复现出了库存-1的情况。这个被无数电商系统讲烂了的并发问题在自己项目里亲眼见到一次印象会非常深。3.2 用条件更新一次性完成校验扣减解决超卖最直接有效的方式是把校验库存和扣减库存合并成一条SQL利用数据库的行锁和条件判断来保证原子性做法是这样的UPDATE inventory SET available available - 1 WHERE book_id ? AND available 0;这条SQL的含义是只有当当前可售库存大于0时才执行减一的操作。如果库存不够执行后影响的行数是0代码里判断一下影响行数就知道库存扣减失败了直接返回库存不足即可不需要提前SELECT也不用加复杂的锁。实际项目里下单时需要同时操作inventory表和orders表我的建议是开启事务先执行上面这条UPDATE如果影响行数为0事务回滚直接返回库存不足如果影响行数为1继续插入订单表和订单项表然后提交事务。这样一个完整的下单流程下来库存和订单的数据始终是一致的不会被并发请求打破。3.3 锁定库存与状态机待付款订单要占住可售数量前面讲到库存分成了available和locked那么下单时的库存操作不是简单的available减一而是两行一起处理UPDATE inventory SET available available - 1, locked locked 1 WHERE book_id ? AND available 0;当用户付款成功再执行UPDATE inventory SET locked locked - 1, sold sold 1 WHERE book_id ?;如果订单被取消或者超时关闭反向操作UPDATE inventory SET locked locked - 1, available available 1 WHERE book_id ?;这套逻辑本身不复杂但需要配合订单状态机一起工作。因为下单成功只是锁库存付款成功才是真正扣减库存两个环节之间可能隔着十几分钟甚至半小时这期间库存必须一直被锁定着不能再卖给其他人。3.4 超时关单和支付回调的幂等处理有了锁定库存的概念自然会引出另一个问题用户下单后不付款怎么办我的方案是加一个定时任务每分钟扫描一次订单表把创建时间超过30分钟仍未付款的订单统一标记为已取消同时释放对应的locked库存。这里有个细节定时任务里要加上条件只处理待付款状态的订单。因为订单可能在定时任务扫描前已经被用户主动取消了或者刚好在扫描的瞬间支付成功如果没有状态判断就可能把已支付或已取消的订单再取消一遍造成库存重复释放。我在这块的处理是取消订单的SQL里带上status 待付款条件影响行数为0就直接跳过保证操作是幂等的。支付回调同样要处理幂等。用户付款成功后支付平台可能会重复推送回调通知如果回调处理不做状态判断库存sold就会被重复累加。我在回调接口里的第一件事就是查一次订单当前状态如果已经不是待付款了直接返回成功不再重复处理。4. 前台展示和检索让读者更快找到想买的书4.1 搜索和分类个人项目不需要上来就上全文检索前台是买家唯一能接触到的部分体验好不好直接影响会不会下单。检索功能是网上书店的刚需但我建议个人项目第一版不用一上来就搞全文检索、分词引擎之类的东西除非你的书目已经超过几万条。我用的方案很朴素一个搜索接口支持按书名、作者、出版社三个字段做模糊匹配核心SQL长这样SELECT * FROM books WHERE status 1 AND (title LIKE CONCAT(%, ?, %) OR author LIKE CONCAT(%, ?, %) OR publisher LIKE CONCAT(%, ?, %)) ORDER BY created_at DESC LIMIT ? OFFSET ?;实际测试下来在几千条图书数据里这种like查询的响应时间基本在毫秒级别完全够用。配合category_id做分类筛选再按价格、出版时间、上架时间排序已经能满足绝大多数找书场景。有个小细节很多人会忽略搜索结果页默认只展示在售状态的书也就是status等于1。否则会出现用户搜到一本书点进去发现已下架体验很不好。这个过滤条件放在SQL里加上简单又有效。4.2 购物车、库存与下单页的联动购物车是典型的意愿清单它不应该也不可能锁定库存。我给购物车设计的原则是购物车里所有商品数量都可以随意增删参考的是当前可售库存的展示值但真正校验库存一定在下单接口里做第二次。为什么下单接口必须再查一次库存因为购物车页面上显示的库存是用户上次加载页面时的数据从加入购物车到点击下单中间可能隔着几天那本书可能已经卖完了。我第一次测试的时候就遇到过这种情况购物车里放了一本书三天后再结算页面数据没刷新下单接口如果不加校验就会超卖。所以下单接口的正确流程是读取购物车选中的条目逐条校验库存是否足够如果某本书库存不足直接返回提示并让用户去掉那本书全部通过后才开始执行前面讲到的锁定库存和生成订单。也就是说前台的库存展示只能说仅供参考真正的把关者是后端下单接口。4.3 一个不起眼但很拉好感的小功能缺货登记网上书店运营一段时间后一定会遇到有货的时候没人买、卖完了反而有人问的情况。我后来加了一个很小的功能当一本书可售库存为0时详情页显示暂时缺货用户可以留下邮箱做缺货登记后台管理员补货成功后在后台标记已到货系统给登记的邮箱发一封提醒邮件。这个小功能代码量很少一个登记表、一个后台标记、一封邮件但对个人书店的客户体验帮助很大。用户会觉得这家店不是挂在那里不理人的是真的在帮忙找书。如果你做这个项目是为了积累作品这种细节也更容易打动看的人。5. 上线部署与日常运维个人项目也要有备份意识5.1 一台云主机足够撑起整个项目个人网上书店的流量不会很大部署架构没有必要设计得很复杂。我用了一个最朴素的方案一台云主机上面装Nginx处理静态文件、反向代理、挂载SSL证书、应用服务跑后端接口和MySQL数据存储三个组件全部跑在同一台机器上。实际上线时主要有两个注意点。第一个是申请一个域名并且配上免费的SSL证书现在浏览器对没HTTPS的网站非常不友好用户看到不安全的提示基本就流失了。第二个是Nginx配置里要把上传图片的大小限制放开一点因为图书封面图动辄几百KB到几MB默认限制经常会导致上传失败。部署的时候我习惯先在本机把整个流程跑通再上服务器顺序大概是装好依赖、导入数据库表结构、启动应用、Nginx配置反向代理、申请证书、用域名访问测试。整个过程如果顺利两三个小时就能完成。5.2 定时备份与恢复演练个人项目最容易犯的毛病是认为数据不会丢。我做网上书店期间给自己定了一个规矩每天凌晨用mysqldump全量备份一次数据库备份文件保留7天用脚本自动清理过期文件。下面是一个可以改改就用的定时备份脚本#!/bin/bash DATE$(date %Y%m%d) mysqldump -u备份用户 -p密码 bookstore /backup/bookstore_$DATE.sql find /backup -name bookstore_*.sql -mtime 7 -delete备份脚本写完之后我强烈建议大家真的做一次恢复演练。我自己的经历是第一次做恢复测试时发现备份文件只有几KB打开一看是空文件原因是mysqldump命令里漏了账号权限配置。如果没有提前演练等到哪天真的误删数据才发现备份不可用哭都来不及。定时任务用crontab加上就行0 3 * * * /root/backup.sh每天凌晨三点自动执行设置成半夜是怕备份时影响用户访问。反正个人书店凌晨基本没人访问这个时间点刚刚好。5.3 日志里最容易看出问题的地方网上书店项目上线后最常出问题的两个地方是支付回调异常和订单状态异常。为了让问题能被定位我要求所有关键操作都在应用里写操作日志下单、支付回调、取消订单、发货、后台改订单状态每一条都记录时间、操作内容和当前订单状态。实际排查过一个案例某用户反馈付款成功但订单一直显示待付款。我查了日志发现支付回调确实到达了服务器但处理过程中抛了一个异常订单状态没有更新。如果没有日志这种问题只能靠猜有了日志一眼就能定位。从这个角度说日志不是写给系统看的是写给三天后的自己看的。另外Nginx的访问日志也能看出很多信息比如某个接口响应特别慢、某个页面出现大量404。个人项目没有监控系统日志就是唯一的事实来源遇到问题先查日志再查代码顺序别搞反。最后再分享一个经验个人网上书店这种项目功能永远做不完先把主干跑通再添枝节。我做完第一版之后发现最值得花时间的不是界面做得多花哨而是把下单、支付、库存这一串状态流转在各种异常情况下都能自洽。只要这条主干是稳的后面加功能都是水到渠成的事。