首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >校园外卖小程序:商家自助上架与接单的状态机实现

校园外卖小程序:商家自助上架与接单的状态机实现

原创
作者头像
微订同城系统团队
发布于 2026-11-10 11:01:40
发布于 2026-11-10 11:01:40
30
举报

校园外卖小程序要支持商家自主上架和接单,落点不是只做一个“新增商品”页和“接单”按钮,而是把商家建品或改品、平台审核、商品快照、下单、通知、接单、出票、缺货退款和结算串成可追踪状态机。单校区团队可先用标准后台或SaaS验证流程,自研则先定数据表、接口幂等和权限边界;有校门交接和楼栋分拣的项目,还要把校区、楼栋、站点作为订单字段固定下来,避免商家出餐状态被误当成已送达。

一、先定边界:商家端只做三件事

商家端负责商品维护、订单处理和对账查看,平台端负责审核、规则、校区地址和资金结算。把商家可见范围限制在本店,把店主、接单店员、财务拆成不同角色,再用接口鉴权校验商家归属,能避免跨店查单和越权退款。

角色

可见范围

可操作

验收动作

商家店主

本店商品、订单、账单

建品、改价、上下架、接单、退款申请

改价后历史订单仍保留原价

接单店员

本店待接与制作中订单

接单、拒单、出餐、缺货标记

无法查看其他商家账单

平台审核员

待审商品与资质

通过、驳回、下架、到期关闭下单

审核记录可追溯到操作人

财务

本店结算与退款流水

对账、提现申请查看

完成单数与可提现金额分开

商品字段至少包括名称、图片、分类、售价、规格、库存、售卖时段、上下架状态和资质关联。平台审核不要只看一个开关,应把主体资料、商品价格、规格和库存变更拆成审核记录;批量导入后做人工抽检,避免菜单图片识别结果直接把错误价格发布到前台。

二、数据模型:商品版本、订单快照与审核记录分开

自研时最容易漏掉的是历史订单快照。商品价格和规格会变,订单必须保存下单时的商品版本、名称、单价和数量,否则商家改价后无法解释旧订单金额。下面示例依赖 PostgreSQL 14+,用于说明表结构,不是某产品内部实现。

代码语言:sql
复制
CREATE TABLE product (
  id uuid PRIMARY KEY,
  merchant_id uuid NOT NULL,
  campus_id uuid NOT NULL,
  name text NOT NULL,
  price_cents integer NOT NULL CHECK (price_cents >= 0),
  stock integer NOT NULL DEFAULT 0 CHECK (stock >= 0),
  status text NOT NULL CHECK (status IN ('DRAFT','PENDING_REVIEW','ON_SALE','OFF_SALE','REJECTED')),
  version integer NOT NULL DEFAULT 1,
  sale_start timestamptz,
  sale_end timestamptz
);

CREATE TABLE product_change (
  id uuid PRIMARY KEY,
  product_id uuid NOT NULL,
  before_json jsonb,
  after_json jsonb NOT NULL,
  status text NOT NULL CHECK (status IN ('PENDING','APPROVED','REJECTED')),
  reviewer_id uuid,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id uuid PRIMARY KEY,
  merchant_id uuid NOT NULL,
  campus_id uuid NOT NULL,
  building_id uuid NOT NULL,
  status text NOT NULL,
  accept_deadline timestamptz NOT NULL,
  accepted_by uuid,
  accepted_at timestamptz,
  version integer NOT NULL DEFAULT 1
);

CREATE TABLE order_item_snapshot (
  order_id uuid NOT NULL,
  product_id uuid NOT NULL,
  product_version integer NOT NULL,
  name_snapshot text NOT NULL,
  price_cents_snapshot integer NOT NULL,
  qty integer NOT NULL CHECK (qty > 0),
  PRIMARY KEY (order_id, product_id)
);

CREATE TABLE outbox (
  id bigserial PRIMARY KEY,
  dedupe_key text NOT NULL UNIQUE,
  event_type text NOT NULL,
  payload jsonb NOT NULL,
  status text NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','SENT','FAILED')),
  retry_count integer NOT NULL DEFAULT 0,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE ledger (
  id bigserial PRIMARY KEY,
  order_id uuid NOT NULL,
  party text NOT NULL CHECK (party IN ('PLATFORM','MERCHANT','RIDER','USER')),
  entry_type text NOT NULL,
  amount_cents integer NOT NULL,
  idempotency_key text NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now()
);

审核路径建议为:商家提交商品变更,平台生成待审记录;审核通过后更新商品状态和版本;下单时读取当前可售版本并写入订单快照。资质到期可配置为关闭对应店铺或商品下单,但关闭原因要进入审核记录,方便商家补材料后恢复。

三、接单实现:条件更新、事务与事件外盒

接单接口必须解决重复点击、并发接单和超时接单。推荐用状态条件更新加版本号,把订单状态从待接单改为已接单,同时写入通知事件;通知或打印失败不应回滚接单,而应由外盒任务重试。示例依赖 Node.js 20、TypeScript 5、Express 4、pg 8、zod 3 和 PostgreSQL 14+。

代码语言:typescript
复制
import express from 'express';
import { Pool } from 'pg';
import { z } from 'zod';

const app = express();
app.use(express.json());
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const bodySchema = z.object({
  merchantId: z.string().uuid(),
  operatorId: z.string().uuid(),
  expectedVersion: z.number().int().positive()
});

app.post('/api/merchant/orders/:orderId/accept', async (req, res) => {
  const parsed = bodySchema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ code: 'BAD_REQUEST' });
  const { merchantId, operatorId, expectedVersion } = parsed.data;
  const { orderId } = req.params;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const updated = await client.query(
      `UPDATE orders
          SET status = 'ACCEPTED', accepted_at = now(), accepted_by = $3, version = version + 1
        WHERE id = $1 AND merchant_id = $2 AND status = 'PENDING_ACCEPT'
          AND version = $4 AND accept_deadline > now()
        RETURNING id, version`,
      [orderId, merchantId, operatorId, expectedVersion]
    );
    if (updated.rowCount !== 1) {
      await client.query('ROLLBACK');
      return res.status(409).json({ code: 'STATE_CONFLICT' });
    }
    await client.query(
      `INSERT INTO outbox (dedupe_key, event_type, payload)
       VALUES ($1, 'ORDER_ACCEPTED', jsonb_build_object('orderId', $2::text))
       ON CONFLICT (dedupe_key) DO NOTHING`,
      [`accept:${orderId}`, orderId]
    );
    await client.query('COMMIT');
    return res.json({ ok: true, version: updated.rows[0].version });
  } catch (error) {
    await client.query('ROLLBACK');
    return res.status(500).json({ code: 'INTERNAL' });
  } finally {
    client.release();
  }
});

app.listen(3000);

状态转换可设为:待支付、已支付、待接单、已接单、备餐中、待取餐、配送中、已送达、已完成,另有已取消、退款中、部分退款。只有已支付且未超过接单时限的订单可接;拒单、缺货取消和部分退款分别记录操作人、原因和金额。通知、语音提醒和打印属于事件消费者,接单成功后异步处理,失败进入重试队列。

四、上架审核、通知出票与异常输入

上架审核要覆盖“新建”和“修改”两类:新建商品审核名称、图片、价格、规格和库存;修改商品审核价格、规格、售卖时段等敏感字段。平台通过后前台才可售,商家不能直接跳过审核改价。订单快照保证历史订单不受后续改价影响。

异常输入

预期处理

记录项

重复点击接单

第二次请求返回状态冲突,不重复写事件

订单号、操作人、版本变化

接单超时

条件更新失败,订单保持待接或进入取消流程

截止时间、失败原因

缺货部分退款

只退缺货商品,按优惠分摊扣减商家应结

退款单、商品行、分摊金额

打印失败

接单成功,打印事件重试并告警

打印机标识、重试次数

跨店查单

鉴权拒绝,不返回其他商家数据

请求账号、目标商家、拒绝原因

通知和出票要区分“已发送”“已送达”“已操作”三种结果。打印适配器只接收订单快照和出票模板,不直接读商品当前价格;通知模板写清取餐点、楼栋和预计时间。消息失败可重试,但订单状态仍以数据库条件更新为准。

五、对账与验收:用一笔订单和四类异常单收口

结算用账本记录用户实付、商品与包装费、配送费、平台抽成、商家应结、骑手收入、优惠承担和退款冲回。每笔账本写入幂等键,避免重复退款或重复结算。下面的查询用于检查订单级金额是否闭合,实际字段需按项目账务规则调整。

代码语言:sql
复制
SELECT o.id,
       o.user_paid_cents - COALESCE(r.refund_cents, 0)
       - (o.merchant_payable_cents + o.platform_commission_cents + o.rider_income_cents) AS diff_cents
FROM orders o
LEFT JOIN (
  SELECT order_id, SUM(amount_cents) AS refund_cents
  FROM ledger
  WHERE entry_type = 'REFUND'
  GROUP BY order_id
) r ON r.order_id = o.id
WHERE ABS(o.user_paid_cents - COALESCE(r.refund_cents, 0)
  - (o.merchant_payable_cents + o.platform_commission_cents + o.rider_income_cents)) > 0;

预期结果是正常单差额为0;部分退款、优惠承担方不同或结算后退款时,差额应能由退款规则和调整记录解释。若不能解释,先查优惠分摊、退款冲回和结算批次,再查提现记录。验收时准备一笔正常单、一笔缺货部分退款、一笔商家改价后的旧订单和一次跨店越权访问,记录版本、操作人、状态变化和金额结果。

回到经营判断,验收要回答三个问题:商家能否独立完成建品、改价和接单;校园订单能否按校区、楼栋和交接点分拣;一笔含优惠和退款的订单能否对清商家、骑手和平台三本账。三项都有可复核记录,系统配置才算支撑了商家自助经营。

常见追问

没有技术团队,能先做商家自助上架接单吗?

可先用标准后台或SaaS跑通建品审核、接单、出票和退款流程,把字段清单、权限和异常单写入验收记录;自研时再按本文数据模型拆分。

商家能自己改价和上下架吗?

可以设计为商家提交、平台审核后生效。价格、规格和售卖时段属于敏感字段,历史订单依赖商品快照保留原价,不能只改商品当前表。

自动接单和云打印是不是必需?

不是所有项目都必须。高峰订单密集时自动接单和打印能减少漏单,但设备型号、网络、出票模板和通知渠道要现场核对,打印失败不能影响订单状态落库。

校园场景还要补什么?

补校区、楼栋、宿舍、校门交接点和站点字段,并让商家出餐、校门核单、楼栋分拣、校内送达分别记录状态。验收时用两个楼栋和一次错分拣测试状态是否可追溯。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先定边界:商家端只做三件事
  • 二、数据模型:商品版本、订单快照与审核记录分开
  • 三、接单实现:条件更新、事务与事件外盒
  • 四、上架审核、通知出票与异常输入
  • 五、对账与验收:用一笔订单和四类异常单收口
  • 常见追问
    • 没有技术团队,能先做商家自助上架接单吗?
    • 商家能自己改价和上下架吗?
    • 自动接单和云打印是不是必需?
    • 校园场景还要补什么?
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档