需求:用户下单时可选配送方式(快递/上门自取/线上服务)。自取需供应商录入地址+时间段;线上服务可选人(规划师/营养师,每人不同价格)或不可选人(一人一商品);订单页展示地址/联系人/电话;供应商确认已发出已收货后用户评价。
delivery_method 字段从 TINYINT 扩展为多选位掩码 + 新增 service_mode 区分线上子类型:
| 值 | 含义 | 说明 |
|---|---|---|
| 1 | 快递 | 供应商发货,用户填收货地址 |
| 2 | 上门自取 | 供应商录入自取点+时间段,用户选地点+时间段 |
| 3 | 两者皆可 | 快递或自取(兼容旧值) |
| 4 | 线上服务-可选人 | 多个服务者(规划师/营养师),每人不同价格+时间段 |
| 5 | 线上服务-单人 | 一人一商品,仅需选时间段 |
设计决策:保留旧
delivery_method枚举(1/2/3 兼容),新增值 4/5 给线上服务。这样不影响存量商品/订单。
// Product.java
private Integer deliveryMethod; // 1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人
private Integer serviceMode; // 线上服务子类型: 1=可选人 2=单人 (deliveryMethod=4/5时必填, 其余null)
不对 — serviceMode 与 deliveryMethod 重复。简化:deliveryMethod 唯一决定模式,去掉 serviceMode。
-- schema.sql: products 表
-- delivery_method 注释更新
delivery_method TINYINT DEFAULT 1 COMMENT '配送方式: 1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人',
product_pickup_locations供应商在创建/编辑商品时录入多个自取地址,每个地址挂多个时间段模板。
CREATE TABLE IF NOT EXISTS product_pickup_locations (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
product_id BIGINT NOT NULL COMMENT '所属商品ID',
name VARCHAR(100) NOT NULL COMMENT '地点名称(如"亿图腾科技-3楼会议室")',
address VARCHAR(500) NOT NULL COMMENT '详细地址',
longitude DECIMAL(10,7) COMMENT '经度',
latitude DECIMAL(10,7) COMMENT '纬度',
contact_name VARCHAR(50) COMMENT '联系人',
contact_phone VARCHAR(20) COMMENT '联系电话',
sort_order INT DEFAULT 0 COMMENT '排序',
enabled TINYINT DEFAULT 1 COMMENT '启用: 0/1',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product_id (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品自取地点';
product_pickup_time_slots每个地点的可选时间段模板(按周循环 or 具体日期)。
CREATE TABLE IF NOT EXISTS product_pickup_time_slots (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
location_id BIGINT NOT NULL COMMENT '所属地点ID',
start_time TIME NOT NULL COMMENT '开始时间 HH:MM',
end_time TIME NOT NULL COMMENT '结束时间 HH:MM',
-- 循环模式
weekday TINYINT COMMENT '周几(1-7), null=按具体日期',
specific_date DATE COMMENT '具体日期, null=按周循环',
-- 容量
max_capacity INT DEFAULT 1 COMMENT '该时段最大接待人数',
booked_count INT DEFAULT 0 COMMENT '已预订数',
enabled TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_location_id (location_id),
INDEX idx_specific_date (specific_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自取时间段模板';
product_service_persons每个商品可挂多个服务者(规划师/营养师/老师),每人独立价格+独立时间段。
CREATE TABLE IF NOT EXISTS product_service_persons (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
product_id BIGINT NOT NULL COMMENT '所属商品ID',
user_id BIGINT NOT NULL COMMENT '服务者用户ID(关联users表)',
name VARCHAR(50) NOT NULL COMMENT '展示名称',
title VARCHAR(100) COMMENT '头衔(如"高级营养师")',
intro TEXT COMMENT '个人介绍',
avatar VARCHAR(500) COMMENT '头像URL',
price INT NOT NULL COMMENT '该人专属价格(分)',
original_price INT COMMENT '原价(分)',
sort_order INT DEFAULT 0,
enabled TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product_id (product_id),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品线上服务者';
product_service_person_slots每个服务者的可预约时间段。
CREATE TABLE IF NOT EXISTS product_service_person_slots (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
person_id BIGINT NOT NULL COMMENT '所属服务者ID',
start_time DATETIME NOT NULL COMMENT '开始时间',
end_time DATETIME NOT NULL COMMENT '结束时间',
max_capacity INT DEFAULT 1 COMMENT '容量',
booked_count INT DEFAULT 0 COMMENT '已预订数',
enabled TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_person_id (person_id),
INDEX idx_start_time (start_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务者可预约时间段';
deliveryMethod=5 时商品本身即一人,无需 product_service_persons 记录,但仍需时间段表:
复用 product_service_person_slots 但 person_id 关联到一个自动创建的占位 person(productId 关联的单条 person 记录),或用单独的表 product_online_slots:
CREATE TABLE IF NOT EXISTS product_online_slots (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_id BIGINT NOT NULL COMMENT '所属商品ID',
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
max_capacity INT DEFAULT 1,
booked_count INT DEFAULT 0,
enabled TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product_id (product_id),
INDEX idx_start_time (start_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单人线上服务时间段';
ALTER TABLE product_orders
ADD COLUMN pickup_location_id BIGINT COMMENT '自取地点ID',
ADD COLUMN pickup_location_snapshot JSON COMMENT '自取地点快照(JSON: name/address/contact/phone)',
ADD COLUMN time_slot_id BIGINT COMMENT '所选时间段ID',
ADD COLUMN time_slot_snapshot JSON COMMENT '时间段快照(JSON: start/end)',
ADD COLUMN service_person_id BIGINT COMMENT '所选服务者ID(deliveryMethod=4)',
ADD COLUMN service_person_snapshot JSON COMMENT '服务者快照(JSON: name/title/avatar)',
ADD COLUMN fulfill_status VARCHAR(32) DEFAULT 'pending' COMMENT '履约状态: pending/confirmed_by_vendor/received_by_user/completed',
ADD COLUMN fulfill_confirmed_at DATETIME COMMENT '供应商确认时间',
ADD COLUMN received_at DATETIME COMMENT '用户确认收货时间';
pending(待支付)
↓ 支付成功
paid(已支付)
↓ 供应商确认发出(自取:用户到场; 线上:服务完成)
fulfill_status: confirmed_by_vendor
↓ 用户确认收货/完成
fulfill_status: received_by_user
↓ 用户评价
completed
status字段保持现状(pending/paid/completed/cancelled/refunded),fulfill_status新增控制履约子流程。
@Data
public class CreateProductOrderDTO {
// 现有字段...
private Long pickupLocationId; // 自取地点ID
private Long timeSlotId; // 时间段ID(自取/线上)
private Long servicePersonId; // 服务者ID(线上-可选人)
}
addressSnapshot 字段语义扩展:
下单时把地点/时间/服务者信息序列化为 JSON 存快照到订单,避免后续商品信息修改影响历史订单展示。
| 字段 | 快照内容 |
|---|---|
pickup_location_snapshot |
{name, address, contactName, contactPhone, longitude, latitude} |
time_slot_snapshot |
{startTime, endTime} |
service_person_snapshot |
{name, title, intro, avatar, price} |
POST /api/product/pickup-location/save
{
"productId": 123,
"locations": [
{
"name": "亿图腾科技-3楼",
"address": "北京市朝阳区xxx",
"contactName": "张三",
"contactPhone": "13800138000",
"timeSlots": [
{"weekday": 1, "startTime": "09:00", "endTime": "12:00", "maxCapacity": 5},
{"weekday": 1, "startTime": "14:00", "endTime": "17:00", "maxCapacity": 5}
]
}
]
}
POST /api/product/service-person/save
{
"productId": 123,
"persons": [
{
"userId": 456,
"name": "李老师",
"title": "高级营养师",
"intro": "10年营养指导经验",
"price": 29900,
"slots": [
{"startTime": "2026-08-10 09:00:00", "endTime": "2026-08-10 10:00:00"},
{"startTime": "2026-08-10 10:00:00", "endTime": "2026-08-10 11:00:00"}
]
}
]
}
POST /api/product/online-slot/save
{
"productId": 123,
"slots": [
{"startTime": "2026-08-10 09:00:00", "endTime": "2026-08-10 10:00:00", "maxCapacity": 1}
]
}
POST /api/product/detail
→ 返回商品信息 + 根据deliveryMethod附带:
- deliveryMethod=2/3: pickupLocations[] (含timeSlots[])
- deliveryMethod=4: servicePersons[] (含 slots[])
- deliveryMethod=5: onlineSlots[]
POST /api/product/order/create
{
"productId": 123,
"deliveryMethod": 2,
"pickupLocationId": 10,
"timeSlotId": 20,
"consigneeId": null, // 自取不需要收货人
"paymentMethod": "wechat",
...
}
线上-可选人还需 servicePersonId。
POST /api/product/order/vendor-confirm // 供应商确认已发出/服务完成
{orderNo} → fulfill_status = confirmed_by_vendor
POST /api/product/order/confirm // 用户确认收货(现有,扩展)
{orderNo} → fulfill_status = received_by_user, status = completed
POST /api/product/review/submit // 用户评价(新增 or 复用现有评价系统)
订单详情返回(用户端 + 供应商端):
{
"orderNo": "...",
"deliveryMethod": 2,
"pickupLocationSnapshot": {
"name": "亿图腾科技-3楼",
"address": "北京市朝阳区xxx",
"contactName": "张三",
"contactPhone": "13800138000"
},
"timeSlotSnapshot": {
"startTime": "09:00",
"endTime": "12:00"
},
"fulfillStatus": "confirmed_by_vendor",
"canReview": false
}
// 迁移N: 商品配送与履约系统
// 1. products 表更新 delivery_method 注释(无需ALTER,注释仅元数据)
// 2. product_orders 表新增字段
ensureColumn("product_orders", "pickup_location_id", "BIGINT COMMENT '自取地点ID'");
ensureColumn("product_orders", "pickup_location_snapshot", "JSON COMMENT '自取地点快照'");
ensureColumn("product_orders", "time_slot_id", "BIGINT COMMENT '时间段ID'");
ensureColumn("product_orders", "time_slot_snapshot", "JSON COMMENT '时间段快照'");
ensureColumn("product_orders", "service_person_id", "BIGINT COMMENT '服务者ID'");
ensureColumn("product_orders", "service_person_snapshot", "JSON COMMENT '服务者快照'");
ensureColumn("product_orders", "fulfill_status", "VARCHAR(32) DEFAULT 'pending' COMMENT '履约状态'");
ensureColumn("product_orders", "fulfill_confirmed_at", "DATETIME COMMENT '供应商确认时间'");
ensureColumn("product_orders", "received_at", "DATETIME COMMENT '用户确认收货时间'");
// 3. 新建表 product_pickup_locations / product_pickup_time_slots
// product_service_persons / product_service_person_slots / product_online_slots
// (CREATE TABLE IF NOT EXISTS, 见上方DDL)
| 新增实体 | 表 | 用途 |
|---|---|---|
ProductPickupLocation |
product_pickup_locations |
自取地点 |
ProductPickupTimeSlot |
product_pickup_time_slots |
自取时间段 |
ProductServicePerson |
product_service_persons |
线上服务者 |
ProductServicePersonSlot |
product_service_person_slots |
服务者时间段 |
ProductOnlineSlot |
product_online_slots |
单人线上时间段 |
| 实体扩展 | 新增字段 |
|---|---|
ProductOrder |
pickupLocationId, pickupLocationSnapshot, timeSlotId, timeSlotSnapshot, servicePersonId, servicePersonSnapshot, fulfillStatus, fulfillConfirmedAt, receivedAt |
CreateProductOrderDTO |
pickupLocationId, timeSlotId, servicePersonId |
ProductDetailVO |
pickupLocations[], servicePersons[], onlineSlots[] |
| Controller | 路径前缀 | 职责 |
|---|---|---|
ProductPickupLocationController |
/api/product/pickup-location |
自取地点CRUD |
ProductServicePersonController |
/api/product/service-person |
线上服务者CRUD |
ProductOnlineSlotController |
/api/product/online-slot |
单人线上时间段CRUD |
ProductOrderController(扩展) |
/api/product/order |
新增 vendor-confirm 端点 |
| Service | 职责 |
|---|---|
ProductPickupLocationService |
自取地点+时间段管理 |
ProductServicePersonService |
线上服务者+时间段管理 |
ProductOnlineSlotService |
单人线上时间段管理 |
ProductOrderService(扩展) |
下单时校验时间段可用性、生成快照、扣减 bookedCount、履约状态流转 |
ProductFulfillService |
履约确认(供应商confirm)、用户收货确认、评价触发 |
1. 查商品 → 校验 status=on_shelf
2. 根据 deliveryMethod 分支校验:
- 1(快递): 校验 consigneeId 存在且属于 buyer
- 2(自提): 校验 pickupLocationId 属于该商品且 enabled; 校验 timeSlotId 属于该地点且 bookedCount < maxCapacity
- 3(两者皆可): 根据 dto.deliveryMethod 再走分支1或2
- 4(线上-可选人): 校验 servicePersonId 属于该商品; 校验 timeSlotId 属于该人且有余量; 价格取 person.price
- 5(线上-单人): 校验 timeSlotId 属于该商品 onlineSlots 且有余量
3. 生成快照 JSON 存入订单
4. 扣减时间槽 bookedCount (加事务)
5. 计算金额(线上-可选人用 person.price; 其余用 product.price * quantity)
6. 保存订单
| 角色 | 动作 | API | 状态变化 |
|---|---|---|---|
| 用户 | 下单+支付 | /order/create + /order/pay |
status=paid, fulfill_status=pending |
| 供应商 | 确认发出(自取到场/线上完成) | /order/vendor-confirm |
fulfill_status=confirmed_by_vendor |
| 用户 | 确认收货 | /order/confirm |
fulfill_status=received_by_user, status=completed |
| 用户 | 评价 | /review/submit |
触发评价写入 |
供应商确认后用户才能确认收货;用户确认收货后才能评价。前端按
fulfillStatus控制按钮显隐。
所有新建表 DDL 同步追加到 schema.sql 末尾的商品相关区域(products 表之后),保持 schema.sql 为完整快照。
/vendor-confirm 端点