本地生活平台搭建:统一订单表与多后台切换怎么拆

地级市团队做本地生活平台搭建 ,技术评审里常有一个分叉:业务模块可以分期上线,但订单与用户核心资料能不能共用一张表、一套写入口 ?若外卖、跑腿、同城团购各维护 order_main,运营就要在多个后台之间切换;同一用户在不同模块里是不同 user_id,营销和对账只能人工拼。

下文从表结构、状态机写路径、Service 分层说明「统一订单中枢 + 多业态扩展」怎么拆。示例为教学示意,以光合同城当期交付为准。

痛点:多后台根因往往是多写入口

早期拼装方案常见结构:

  • 外卖模块自带 wm_order,跑腿模块自带 errand_order,团购模块自带 group_order
  • 各模块独立 Admin 控制台,运营按业态切换 URL
  • 用户表按模块复制,靠手机号后匹配「假装是同一人」

后果很直观:改配送状态进 A 后台,财务导出在 B 后台;加营销券要在三个系统各配一遍。本地生活平台搭建 若目标是统一后台一体化,架构上应先收敛写路径,再谈 UI 有多少菜单。

地级市评审时可先问供应商三个问题:订单主表是否全业态共用?状态迁移是否只有一个 Service 写入口?Admin 是否按 biz_type 筛单而不是按模块分域名?答不上来,后面运营多半要在多个后台之间切换。

用户与商家核心资料:禁止按模块复制

多后台方案常伴随「每个模块一张 user 表」。正确做法是 mid-shared 层维护用户商家核心资料,各业态只引用 ID。

sql 复制代码
CREATE TABLE um_user (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  mobile       VARCHAR(20) NOT NULL UNIQUE,
  nickname     VARCHAR(64) NULL,
  member_level TINYINT NOT NULL DEFAULT 0,
  city_code    VARCHAR(12) NOT NULL,
  created_at   DATETIME NOT NULL,
  updated_at   DATETIME NOT NULL
);

CREATE TABLE um_merchant (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  name         VARCHAR(128) NOT NULL,
  city_code    VARCHAR(12) NOT NULL,
  status       VARCHAR(16) NOT NULL COMMENT 'active|suspended|closed',
  settle_mode  VARCHAR(16) NOT NULL,
  created_at   DATETIME NOT NULL
);

-- 商家开通哪些业态,用关联表表达,而不是再建 merchant 副本
CREATE TABLE um_merchant_biz (
  merchant_id  BIGINT NOT NULL,
  biz_type     VARCHAR(16) NOT NULL,
  enabled      TINYINT NOT NULL DEFAULT 1,
  PRIMARY KEY (merchant_id, biz_type)
);

本地生活平台搭建 验收时可固定一个测试手机号:在 takeout 下单后在 group_buy 再下一单,断言两笔单的 user_id 相同。若靠运营后台手工「绑定手机号」才算同一人,说明用户商家资料未打通,后续营销券无法跨业态复用。

模块目录:订单中枢与业态插件分离

text 复制代码
local-life-platform/
├── apps/
│   ├── user-app/                  # 用户端:下单、取消、评价
│   ├── merchant-app/              # 商家端:接单、拒单、出餐/核销
│   ├── rider-app/                 # 配送端:取货、在途、送达
│   └── admin-console/             # 统一运营后台:跨业态筛单、导出
├── order-hub/                     # 订单中枢(唯一写入口)
│   ├── command-api/
│   ├── state-machine/
│   ├── read-model/
│   └── event-outbox/
├── biz-plugins/
│   ├── takeout/                   # 外卖扩展字段与履约策略
│   ├── errand/                    # 跑腿扩展
│   └── group-buy/                 # 同城团购扩展
├── mid-shared/
│   ├── user-master/               # 统一用户主数据
│   ├── merchant-master/
│   └── marketing-core/            # 券、满减、会员标签
└── ops/
    ├── state-transition.yaml
    └── biz-type-routing.yaml

验收要点:takeouterrand 等插件禁止 直连数据库 UPDATE 主订单表;所有状态迁移经 order-hub/command-api

统一订单表:主表 + 业态扩展表

主表存跨业态通用字段;业态差异放扩展表,避免为每个模块复制整张订单。

sql 复制代码
-- 订单主表:全业态共用
CREATE TABLE ol_order (
  id              BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_no        VARCHAR(32) NOT NULL UNIQUE,
  biz_type        VARCHAR(16) NOT NULL COMMENT 'takeout|errand|group_buy|...',
  user_id         BIGINT NOT NULL,
  merchant_id     BIGINT NOT NULL,
  city_code       VARCHAR(12) NOT NULL,
  status          VARCHAR(32) NOT NULL,
  state_version   INT NOT NULL DEFAULT 0 COMMENT '乐观锁',
  pay_amount      DECIMAL(12,2) NOT NULL DEFAULT 0,
  created_at      DATETIME NOT NULL,
  updated_at      DATETIME NOT NULL,
  INDEX idx_user_biz (user_id, biz_type),
  INDEX idx_merchant_status (merchant_id, status)
) COMMENT='本地生活统一订单主表';

-- 外卖扩展:仅 takeout 订单有行
CREATE TABLE ol_order_takeout_ext (
  order_id        BIGINT PRIMARY KEY,
  delivery_type   VARCHAR(16) NOT NULL COMMENT 'platform|rider|self',
  expect_time     DATETIME NULL,
  rider_id        BIGINT NULL,
  CONSTRAINT fk_takeout_order FOREIGN KEY (order_id) REFERENCES ol_order(id)
);

-- 跑腿扩展
CREATE TABLE ol_order_errand_ext (
  order_id        BIGINT PRIMARY KEY,
  pickup_addr     VARCHAR(256) NOT NULL,
  dropoff_addr    VARCHAR(256) NOT NULL,
  goods_weight_kg DECIMAL(6,2) NULL,
  CONSTRAINT fk_errand_order FOREIGN KEY (order_id) REFERENCES ol_order(id)
);

-- 状态变更审计:只追加
CREATE TABLE ol_order_state_log (
  id              BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_id        BIGINT NOT NULL,
  from_status     VARCHAR(32) NOT NULL,
  to_status       VARCHAR(32) NOT NULL,
  actor_type      VARCHAR(16) NOT NULL COMMENT 'user|merchant|rider|admin|system',
  actor_id        BIGINT NOT NULL,
  command         VARCHAR(64) NOT NULL,
  created_at      DATETIME NOT NULL,
  INDEX idx_order_time (order_id, created_at)
);

设计约束:

  1. user_idmerchant_id 引用 mid-shared 主数据,禁止各模块自建用户副本。
  2. biz_type 决定加载哪张扩展表;Admin 控制台按 biz_type 筛单,但 URL 仍是同一套。
  3. 所有 status 变更只经 state-machine,并写 ol_order_state_log

状态机:按履约语义而非按端命名

外卖与跑腿可共用上层状态,差异在扩展字段与合法迁移分支:

yaml 复制代码
# ops/state-transition.yaml(示意)
common:
  - from: CREATED
    to: PAID
    actor: system
    command: pay_success
  - from: PAID
    to: MERCHANT_ACCEPTED
    actor: merchant
    command: accept_order
  - from: MERCHANT_ACCEPTED
    to: COMPLETED
    actor: system
    command: fulfill_done

takeout_only:
  - from: MERCHANT_ACCEPTED
    to: PREPARING
    actor: merchant
    command: start_preparing
  - from: PREPARING
    to: RIDER_DELIVERING
    actor: rider
    command: start_delivery

errand_only:
  - from: MERCHANT_ACCEPTED
    to: RIDER_PICKUP
    actor: rider
    command: confirm_pickup
  - from: RIDER_PICKUP
    to: RIDER_DELIVERING
    actor: rider
    command: start_delivery

启动时校验:每个 biz_type 的迁移图无孤儿状态;非法迁移在 command-api 层直接拒绝。

Service 伪代码:唯一写入口

java 复制代码
@Service
public class OrderCommandService {

    public OrderView acceptOrder(AcceptOrderCmd cmd, Operator op) {
        Order order = orderRepo.findByIdForUpdate(cmd.getOrderId());
        stateMachine.assertTransition(order, "MERCHANT_ACCEPTED", op);

        order.setStatus("MERCHANT_ACCEPTED");
        order.setStateVersion(order.getStateVersion() + 1);
        orderRepo.save(order);

        stateLogRepo.append(order.getId(), cmd.getFromStatus(),
            "MERCHANT_ACCEPTED", op, "accept_order");

        outbox.publish(new OrderStatusChangedEvent(order.getId(),
            order.getBizType(), order.getStatus()));

        return readModel.toView(order);
    }
}

读侧 可按端裁剪字段(用户端不返回内部结算字段),但 order_idstate_version 必须一致。各端缓存通过 event-outbox 失效,避免「骑手端已送达、用户端还在备餐中」。

Outbox 与读模型:多端同步不靠口头对齐

写路径收敛后,读侧仍可能缓存订单快照。推荐 Outbox 模式:状态变更与事件写入同一事务,异步投递到各端缓存失效队列。

java 复制代码
@Service
public class OrderOutboxPublisher {

    @Transactional
    public void publishAfterCommit(Long orderId, String bizType, String status) {
        outboxRepo.insert(new OutboxRow(orderId, bizType, status, "OrderStatusChanged"));
        // 定时任务或 MQ 消费者拉取 pending 行,广播到 user-app / merchant-app / rider-app
    }
}

读模型 read-model 可按端做 VO 裁剪,但查询必须带 state_version。客户端收到事件后,若本地版本落后则拉全量;若超前则丢弃,防止乱序。

地级市团队技术评审时可看一条链路:商家在 merchant-app 点「开始备餐」→ command-api 写主表 → state_log 追加 → Outbox 投递 → 用户端列表刷新。任一步绕过 command-api 直连 UPDATE,都应视为架构缺陷。

Admin 控制台:一套入口筛多业态

统一后台的实现要点:

java 复制代码
@RestController
@RequestMapping("/admin/orders")
public class AdminOrderController {

    @GetMapping
    public Page<OrderSummaryVO> list(
        @RequestParam(required = false) String bizType,
        @RequestParam(required = false) String status,
        @RequestParam(required = false) Long merchantId,
        Pageable page) {
        // 同一接口,按 bizType 过滤;不再为每个模块单独部署 Admin
        return orderQueryService.pageUnified(bizType, status, merchantId, page);
    }
}

运营导出 CSV 时,列头来自主表 + 当前 biz_type 对应扩展表,格式统一,减少跨系统拼表。

与中台能力共享的衔接

mid-shared/marketing-core 发券时,只认 user_idbiz_type 白名单,不关心订单在哪个子库:

sql 复制代码
CREATE TABLE mk_coupon_grant (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id      BIGINT NOT NULL,
  template_id  BIGINT NOT NULL,
  biz_scope    VARCHAR(64) NOT NULL COMMENT 'takeout,errand 或 takeout|group_buy',
  valid_from   DATETIME NOT NULL,
  valid_to     DATETIME NOT NULL
);

中台新增一种券模板,已接入 order-hub 的业态只要 biz_scope 匹配即可生效------这就是能力共享 在代码层的落点,而不是每个业务系统各自维护一套 coupon 表。

无中台 vs 有中台:技术对照

无统一订单中枢时,常见实现是三个独立 Spring Boot 工程、三套 Admin 域名、导出 CSV 列头各不同。运营改一条满减,要在三个后台各配;财务周会前人工 VLOOKUP 拼表。

有统一后台一体化实现时,只有一个 order-hub 写入口、一个 Admin 控制台;biz_type 只是筛选项和扩展表路由。加跑腿模块时,新增 ol_order_errand_ext 与 YAML 迁移分支即可, 新建第二套 Admin,也不重导 um_user

这就是本地生活平台搭建 里「统一后台、数据互通、能力共享」在工程上的落点:读者不必被「中台」二字吓到,核心就是一张主订单表、一套写 Service、一个运营入口

评审清单:六项可跑通的检查

  1. 同一 user_idtakeoutgroup_buy 各下一单,主表 ol_order.user_id 一致。
  2. 非法状态迁移(如从 CREATED 直跳 DELIVERED)被 stateMachine 拒绝并写审计。
  3. 商家改价只经 command-api,数据库层无各端直连 UPDATE 权限。
  4. Admin 导出 CSV 列头统一,含 biz_typestate_version
  5. marketing-core 改券模板,两业态下单页均可用,且未开通业态不出现券入口。
  6. 关闭 errand 路由后,代码仍在库但 Admin 筛选项不出现,证明「加模块 ≠ 加后台」。

部署与分期上线建议

地级市本地生活平台搭建首期建议:

  1. 只启用 takeout + group_buy 两个 biz_type,其余插件代码在库但路由关闭。
  2. 用同一 user_id 完成跨业态下单,断言 ol_order.user_id 一致。
  3. 在 Admin 改一条 mk_coupon_grant,验证两业态下单页均可用。
  4. 第三期再加 errand:仅新增 ol_order_errand_ext 与迁移分支,新建第二套 Admin。

光合同城走统一后台一体化成品交付:内置多业务模块,模块数据互通、后台统一管理。上文表结构与状态机为拆解示意;实际字段、插件清单以当期方案为准。支持私有化源码部署与按需定制开发。

适合谁

适合国内地级市技术负责人或本地服务商:要在本地生活平台搭建评审里验「统一订单表 + 单一写入口 + 统一 Admin」,而不是接受「每个业态一套后台、靠手机号后匹配用户」的拼装方案。

不适合只买静态页面、不涉及真实订单写路径的项目------那种需求用静态站即可,但试运营一上量,多写入口的技术债会迅速放大运营成本。

本地生活平台搭建 的技术分水岭,往往不在功能菜单长度,而在订单与用户有没有先收敛到一套中枢。先把 order-hub 与统一 Admin 立住,再按 biz_type 叠插件,比每个模块各起炉灶,后期扩业态时省下的不止是开发人天,还有运营每天少切的那几次后台。

相关推荐
Fluxart.ai8 分钟前
商品多角度图怎么做?Flux Art 从白底图到规格图、包装图的 10 步教程
开发语言·前端·javascript
边境悍匪13 分钟前
springboot常用注解
java·spring boot·学习
前端繁华如梦18 分钟前
React + Three.js 造了一个"乙烯基娃娃"3D 角色编辑器:配方驱动、程序化生成、还能跳舞
前端
繁华若梦75919 分钟前
全项目 Skills 适配:把「会写代码的 Agent」变成「会按你们规范干活的同事」
前端
江华森33 分钟前
云原生从0到1:Kubernetes 工作负载实战——Deployment/Service/滚动更新/弹性伸缩
前端·后端
江华森40 分钟前
《云原生从0到1:4台华为云ECS搭建Kubernetes 1.28集群实录(上)——环境与踩坑全记录》
前端·后端
captain37641 分钟前
文件与IO(2)
java·开发语言·windows·java-ee
zandy101141 分钟前
网络支付合规三重门:Kimi Code、Cursor、GitHub Copilot等五款AI编程工具国内实战测评
github·copilot·ai编程
codeY44 分钟前
Vite 前端发布后「点击菜单没反应」?旧版本资源 404 的完整排查与修复实录
前端·前端工程化