回首页

Tamora 美妆电商独立站

  • React 19
  • Express 5
  • TypeScript
  • PostgreSQL
  • Prisma
  • BullMQ
  • Vue 3
  • Naive UI
  • Stripe

tamorabeauty.com

一个多品牌美妆集合店,思路类似 Sephora,客户在中东(以迪拜为主)和香港。全部由我一人交付:英文商城前台、API、中文管理后台,服务器与部署,并协助完成 Stripe 商户入驻。已上线。

范围

客户要的是自己的店,而不是入驻第三方平台。前期很多工作是决定什么不做,每一条都连同日期记下来。

  • 支付用 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 个服务。

三种角色:管理员、客服、顾客。退款、员工、审计日志、运费和系统设置只有管理员能动,后台每一次修改都记下改前和改后。

商城前台 React管理后台 Vue一个 APIExpress一套数据PostgreSQL 商城前台 React管理后台 Vue一个 API Express一套数据 PostgreSQL

难点

库存:从「付款后扣」到「下单预占」

最初的规则是付款成功后再扣库存。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 入驻这些手续,花的时间比写代码还多。最有用的一份文档,是一开始就像合同一样定下来的功能清单。

时间线

  1. 02.28第一次需求沟通
  2. 03.01确认支付、登录、WhatsApp 等范围
  3. 04.08建仓库;一天内搭好三端和从购物车到下单的主链路
  4. 04.15测试站跑起来
  5. 05.05后台换成 Naive UI;审计日志、退款、规格变体
  6. 06.25库存预占、Turnstile;迁到生产服务器
  7. 07.15黑盒测试并整改;客户验收通过
  8. 07.27改用 USD、运费线下报价、Stripe 退款同步