一个多品牌美妆集合店,思路类似 Sephora,客户在中东(以迪拜为主)和香港。全部由我一人交付:英文商城前台、API、中文管理后台,服务器与部署,并协助完成 Stripe 商户入驻。已上线。
- 29张表,一套 Prisma 模型
- 7天,从空仓库到测试站跑起来
- 127个提交,全部是我
- 31条范围决策,逐条记录
范围
客户要的是自己的店,而不是入驻第三方平台。前期很多工作是决定什么不做,每一条都连同日期记下来。
- 支付用 Stripe,Apple Pay、Google Pay、Samsung Pay 都通过它接入。不接 PayPal,当地用的人少;不做货到付款,退货风险高。分期付款暂缓,因为网站的主体和当地执照的主体不是同一个。
- 前台英文,后台中文。阿拉伯语本期不做。
- 客服和退换货走 WhatsApp 和邮件,不在站内做聊天和物流追踪。
- 全部代码自己写,不用 Shopify 或 WordPress,客户知情并认可。
- 地址按国际电商通用的结构设计,不写死阿联酋的字段。
系统组成
三端共用一套 29 张表的 Prisma 模型:React 19 商城前台(Vite、React Router、Zustand、Tailwind),按领域拆分模块的 Express 5 API,以及 Vue 3 管理后台,5 月从 Element Plus 换成了 Naive UI。独立的 BullMQ Worker 处理邮件和支付回调;Redis 分成状态和缓存两个实例;图片存在 MinIO。Docker Compose 里共 10 个服务。
三种角色:管理员、客服、顾客。退款、员工、审计日志、运费和系统设置只有管理员能动,后台每一次修改都记下改前和改后。
难点
库存:从「付款后扣」到「下单预占」
最初的规则是付款成功后再扣库存。15 分钟的付款窗口里,两个人可能同时买走最后一件。6 月我改成:下单时在同一个事务里原子地预占每个规格的库存,任何一个不够就整单回滚,付款只做确认。超时、管理员取消、全额退款会回补库存,靠订单状态的原子跃迁保证只补一次;部分退款不回补。有一个并发集成测试专门检查绝不超卖。
Stripe 回调
每个事件先写进一张以事件 ID 为唯一键的表,重复的直接跳过。Worker 用条件更新认领处理权(待处理、失败、或者处理中超过 5 分钟视为卡死),最多试 8 次,每 30 秒处理 25 条。表里只存摘要,不存完整内容。在 Stripe 后台直接做的退款,以 Stripe 的退款列表为准幂等同步回来。下单和支付请求带幂等键,高风险写操作有限流和退款锁。
规格变体是后来补的
第一天约定不做多规格,5 月发现需要。现在用四张表描述(商品、选项、选项值、规格),商品上保留一份默认价格、库存和 SKU 的缓存,让旧代码照常工作。6 月购物车、结算和订单改成按规格 ID 走。这层缓存有代价:8 月的最后一次提交,修的就是一个没同步的价格。
不丢的邮件
订单邮件走发件箱表,每个订单每种邮件一行,记录尝试次数、下次重试时间和锁,由 Worker 投递。密码重置邮件后来也放了进去。
安全地发布
- 生产镜像只能从已推送、工作区干净的提交构建,标签是日期加提交哈希,不能覆盖。只重建有变化的服务。
- 部署时先备份 Compose 配置,所有镜像拉取成功后才改动,失败自动恢复,并持有独占锁。每次发布都记录下来,作为回滚依据;表结构变更先于依赖它的 API 发布。
- CI 对三端跑 lint、测试和构建,共 66 个测试文件。
- 图片:只收 jpg、png、webp,小于 10 MB,按文件头校验,自动旋转,长边限制在 2560 像素,转成 WebP。
- 搜索:一个 hook 统一管理标题、描述、canonical、Open Graph 和 Twitter 卡片;商品页带结构化数据;构建时按线上商品生成 sitemap。
- 所有登录相关接口都有 Turnstile 验证,登录防爆破,退出时服务端吊销会话。
上线之后
7 月我对线上站做了一次黑盒测试:29 个前台公开路由、13 个后台模块。发现了搜索入口跳到 404、后台 Turnstile 因为 sitekey 和 siteKey 写法不一致而没生效、看板筛选链接无效等问题,都修好了。7 月底价格从 AED 改成 USD,运费改为线下报价,在 Stripe 里做的退款也开始同步回来。
修问题不是开发之外的事,它本身就是开发。
耗时最长的反而不是代码。公司资料、Stripe 入驻这些手续,花的时间比写代码还多。最有用的一份文档,是一开始就像合同一样定下来的功能清单。
时间线
- 02.28第一次需求沟通
- 03.01确认支付、登录、WhatsApp 等范围
- 04.08建仓库;一天内搭好三端和从购物车到下单的主链路
- 04.15测试站跑起来
- 05.05后台换成 Naive UI;审计日志、退款、规格变体
- 06.25库存预占、Turnstile;迁到生产服务器
- 07.15黑盒测试并整改;客户验收通过
- 07.27改用 USD、运费线下报价、Stripe 退款同步