# Data Model: 商家自配送 (029) **Date**: 2026-09-08 | 依据: [spec.md](spec.md) + [research.md](research.md) 所有 DDL 写入 `updatesql/sql.md`(标注日期+用途),**不直接执行**(项目规则)。 ## 表变更 ### 1. pos_store(现有表,加 2 列) | 列 | 类型 | 说明 | |----|------|------| | `self_delivery_enabled` | TINYINT(1) NOT NULL DEFAULT 0 | 自配送开关(配置主体=该门店;夜市主门店行开启=整个夜市生效) | | `self_delivery_mode` | TINYINT(1) NULL | 时段模式:NULL/0=未设置,1=全天,2=自定义(按星期);仅 enabled=1 时有意义 | ```sql -- 2026-09-08 029 商家自配送:门店配置 ALTER TABLE pos_store ADD COLUMN self_delivery_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '自配送开关(夜市主门店行开启=旗下摊位统一生效)'; ALTER TABLE pos_store ADD COLUMN self_delivery_mode TINYINT(1) DEFAULT NULL COMMENT '自配送时段模式:1=全天,2=自定义按星期'; ``` ### 2. pos_store_self_delivery_hours(新表,镜像 operating_hours) | 列 | 类型 | 说明 | |----|------|------| | `id` | BIGINT PK AUTO_INCREMENT | | | `md_id` | INT NOT NULL | **配置主体门店 id**(普通门店=自身;夜市=夜市主门店行 id,不是摊位 id) | | `day_of_week` | TINYINT NOT NULL | ISO 星期:1=周一 … 7=周日(对齐 OperatingHours 现有取值) | | `start_time` | VARCHAR(5) NOT NULL | HH:mm | | `end_time` | VARCHAR(5) NOT NULL | HH:mm,必须 > start_time(同段内不允许跨天) | 约束:`UNIQUE (md_id, day_of_week, start_time)`。某天无记录 = 该天无自配送(对齐营业时段"未配即不营业"语义)。 ```sql -- 2026-09-08 029 商家自配送:时段表 CREATE TABLE pos_store_self_delivery_hours ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', md_id INT NOT NULL COMMENT '配置主体门店id(夜市场景为夜市主门店行)', day_of_week TINYINT NOT NULL COMMENT 'ISO星期:1=周一..7=周日', start_time VARCHAR(5) NOT NULL COMMENT '开始时刻 HH:mm(含)', end_time VARCHAR(5) NOT NULL COMMENT '结束时刻 HH:mm(不含)', PRIMARY KEY (id), UNIQUE KEY uk_sd_hours (md_id, day_of_week, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门店自配送时段(按星期)'; ``` ### 3. pos_order(现有表,加 1 列) | 列 | 类型 | 说明 | |----|------|------| | `self_delivery` | TINYINT(1) NOT NULL DEFAULT 0 | 下单判定快照:1=商家自配送(不进骑手池、运费归 sh_id)。终身不变,存量单默认 0 | ```sql -- 2026-09-08 029 商家自配送:订单判定快照 ALTER TABLE pos_order ADD COLUMN self_delivery TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=商家自配送(下单时快照,不进骑手池,运费归sh_id)'; ``` ### 4. user_billing(现有表,无 DDL,启用新 type 值) - 现有用途:type="0" 分成(商家/骑手,dedup 键 userId+type+ddId)、type="3" 到付用户账单。 - **type="5" 固定为自配送运费入账**。原商家端“金额解冻”业务已停止,页面筛选和展示均以“自配送运费”为准;不新增账单类型,避免与商品分成 type="0" 的去重键冲突。 - 字段赋值:`user_id=pos_order.sh_id`(普通商家=商家、夜市摊位单=夜市主,见 research F3)、`amount=freight 全额`、`divvy=0`、`divvy_rate=0`、`user_type="1"`、`md_id`、`dd_id`、`state="0"`、`illustrate=自配送运费(i18n)`。dedup 键沿用 (userId, type, ddId)——type 不同不与商品分成冲突。 - 到付自配送单:送达时用户账单(type="3")`payment_id=sh_id`(镜像骑手送达的 qsId 归因,research F13)。 ## 实体/Mapper 变更(Java) | 层 | 文件 | 变更 | |----|------|------| | Entity | `ruoyi-system/.../domain/PosStore.java` | +selfDeliveryEnabled, +selfDeliveryMode | | Entity | `ruoyi-system/.../domain/PosOrder.java` | +selfDelivery | | Entity(新) | `ruoyi-system/.../domain/PosStoreSelfDeliveryHours.java` | 新实体(MyBatis-Plus 注解风格对齐 OperatingHours) | | Mapper(新) | `ruoyi-system/.../mapper/PosStoreSelfDeliveryHoursMapper.java` | MyBatis-Plus BaseMapper,无需 XML(简单 CRUD) | | Service(新) | `ruoyi-admin/.../order/SelfDeliveryService.java` | 配置解析(含摊位→夜市主门店解析)、时段判定入口、运费入账;仅依赖 system 实体+服务,符合模块边界 | | Util(新) | `ruoyi-admin/.../utils/SelfDeliveryUtil.java` | 纯函数:`matches(hours, enabled, mode, target)`,[start,end) 语义,可单测 | ## 状态机(自配送外送单,type=0) ``` state: 0 待接单 ──商家接单──> 1 已接单 ──商家出餐──> 2 已出餐 ──确认送达──> 3 已完成 deliveryStatus: 0 ────────────────────────────────(可选:开始配送──> 2 配送中)──────> 3 已送达 ``` - 商家接单/出餐:`requireRiderAssigned` 对 `self_delivery=1` 放行(现状 F2 会拦) - "开始配送"(可选):state=2 且 deliveryStatus=0 → deliveryStatus=2;重复操作拒绝 - "确认送达":state=2 且 deliveryStatus∈{0,2} → 原子置 deliveryStatus=3 + state=3 + sdTime=now;随后 completeSideEffects(商品分成 + 自配送运费入账 + 到付归因) - 满足管理端不变量(research F7):delivery=3 ⇒ state=3 - 售后/取消:沿用现有规则,无骑手环节 ## 校验规则(配置保存) 1. 自定义模式必须至少一段时段(否则拒绝) 2. 每段 `end_time > start_time`(字符串比较即可,HH:mm 定长) 3. day_of_week ∈ [1,7];start/end 匹配 `^([01]\d|2[0-3]):[0-5]\d$` 4. 同一星期内时段不得重叠(相邻衔接允许:前段结束=后段开始不算重叠) 5. 同 md_id + 星期 + start 唯一(数据库约束兜底) 6. 保存为整体覆盖式(先删 md_id 全部再插入,与营业时段保存一致)