与住宿包(hotel)、餐饮包(restaurant)同构的第三个行业增量包。
定位:商户自营进销存 + 线下收银。对应 084feature_catalog的retail(标签「零售供货包」,sort_order 40)。
| 核查项 | 结论 | 依据 |
|---|---|---|
复用 008 orders? | 不复用 | order_type CHECK 仅 package/room/ticket/voucher_bundle;user_id NOT NULL。零售线下收银单多为无登录用户的柜台单,塞不进统一订单域 |
复用 inventory.py? | 不复用 | WIP-311 平台库存域:admin_router 写 + C端只读 availability,面向 package/component/route/guide 资源日历,非商户 SKU 进销存 |
复用 miniapp_products.py? | 不复用 | C端旅行套餐适配层,映射老库 packages/package_components,与零售 SKU 无关 |
| 表名冲突? | 无 | prod 库 pg_tables 中 %product%/%retail%/%stock%/%goods% 零命中 |
| SQL 编号 | 用 091 | migrate.sh 以 filename 为 PK(非编号);现存 090_deposits 与 090_restaurant 已重号且无害,但不再新增重号 |
| B端零售页现状 | 占位 | WIP-147「诚实降级」4 页(SupplierProducts/ProductEdit/SupplierApply/SupplierQuestionnaire),零接口、按钮 disabled、文案「接口未开放」 |
| 门禁现状 | 已注册 | business-pack.ts PACK_EXTRA_ROUTES.retail 已含 4 路由;feature_catalog 已有 retail 行 → A端人工开通即可见(09-23 义父定调:不按 business_type 自动锁死) |
| 路由 | 页面 | 职责 |
|---|---|---|
/supplier-products | SupplierProducts(改造) | 商品列表 + 搜索 + 上下架 + 低库存标记 |
/product-edit | ProductEdit(改造) | 商品新增/编辑(品类改为零售品类) |
/retail-stock | RetailStock(新增) | 进货/出货/盘点 + 库存流水 |
/retail-orders | RetailOrders(新增) | 收银开单 + 结账 + 取消回滚库存 |
/retail-dashboard | RetailDashboard(新增) | 今日单量/营业额/SKU 数/低库存预警 |
| 路由 | 原因 |
|---|---|
/supplier-apply | 语义错位:入驻申请是「成为商户之前」的动作,而 B端工作台用户已是商户。后端 miniapp_merchant_applications.py 已完整实现 C端提交 + 运营端审核流(pending→reviewing→approved 自动建 merchants 行)。该页应由 C端/游客侧承接,不属零售包域 |
/supplier-questionnaire | 同上,且后端零问卷资产(全仓无 questionnaire)。属独立问卷域,需先定业务归属再建 |
这两页保留在 PACK_EXTRA_ROUTES.retail 内不动(避免破坏门禁与既有链接),但本次不接真。处置建议见文末「待义父裁定」。
IF NOT EXISTS)价格沿用全仓 *_fen 约定(入参元 → *100,出参 _yuan())。所有表带 is_test 以隔离测试数据;merchant_id 参数化,不写死商户身份。
retail_products — 商品(SKU)| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGSERIAL PK | |
merchant_id | BIGINT NOT NULL FK→merchants ON DELETE CASCADE | |
name | VARCHAR(64) NOT NULL | 商品名 |
description | TEXT DEFAULT '' | |
category | VARCHAR(32) DEFAULT '未分类' | 零售品类(见 §五) |
price_fen | INTEGER ≥0 | 售价 |
cost_fen | INTEGER ≥0 | 成本价(毛利核算用,B端可见、C端不出) |
unit | VARCHAR(16) DEFAULT '件' | 计量单位(件/盒/袋/瓶/斤) |
image_url | VARCHAR(512) DEFAULT '' | |
stock | INTEGER ≥0 DEFAULT 0 | 当前库存 |
stock_warn | INTEGER ≥0 DEFAULT 0 | 低库存预警阈值 |
barcode | VARCHAR(64) DEFAULT '' | 条码(可空,扫码收银预留) |
status | VARCHAR(16) DEFAULT 'onsale' | onsale/offself/soldout |
sort_order | INTEGER DEFAULT 0 | |
is_test | BOOLEAN DEFAULT FALSE | |
created_at/updated_at | TIMESTAMPTZ DEFAULT now() |
索引:(merchant_id, sort_order)、(merchant_id, status);唯一:(merchant_id, barcode) WHERE barcode <> ''(部分唯一索引,避免空条码互斥)。
retail_stock_logs — 库存流水(审计,只增不改)| 字段 | 说明 |
|---|---|
id / product_id FK→retail_products CASCADE / merchant_id | |
change_type | purchase(进货) / sale(销售出库) / adjust(盘点调整) / refund(退货入库) / cancel(撤单回滚) |
qty_change | INTEGER,带符号(进货 +N,销售 −N) |
before_stock / after_stock | INTEGER ≥0,前后快照(对账用) |
ref_order_id | BIGINT NULL,关联 retail_orders.id |
operator_id | BIGINT NULL,操作人 user_id |
note | VARCHAR(255) DEFAULT '' |
is_test / created_at |
索引:(product_id, created_at DESC)、(merchant_id, created_at DESC)。
retail_orders — 零售订单(收银单)| 字段 | 说明 |
|---|---|
id / merchant_id FK CASCADE | |
order_no | VARCHAR(32) NOT NULL,唯一 (merchant_id, order_no);生成规则 T{YYYYMMDD}{6位大写hex}(餐饮 R / 零售 T 区分域) |
status | created(待结账) / paid(已结账) / cancelled(已取消) / refunded(已退款) |
total_fen / paid_fen | INTEGER ≥0 |
pay_method | cash / wechat / alipay / other,DEFAULT cash |
customer_id | BIGINT NULL,可关联既有商户级 guests 表(复用,不重建) |
operator_id | BIGINT NULL |
source | onsite(柜台) / miniapp(线上),DEFAULT onsite |
note | VARCHAR(255) DEFAULT '' |
paid_at | TIMESTAMPTZ NULL |
is_test / created_at / updated_at |
索引:(merchant_id, created_at DESC)、(merchant_id, status)。
retail_order_items — 订单明细(下单时快照,商品改价不影响历史单)| 字段 | 说明 |
|---|---|
id / order_id FK→retail_orders CASCADE | |
product_id | BIGINT NULL(商品软删后仍留痕) |
name | VARCHAR(64) 快照 |
price_fen | INTEGER 快照单价 |
qty | INTEGER ≥1 |
subtotal_fen | INTEGER = price_fen × qty |
is_test / created_at |
索引:(order_id)。
与餐饮 room_meal_plans→menu_items 同一思路:不新建连接表,改为暴露只读目录接口供其他业态引用:
GET /api/miniapp/retail/products?merchant_id=&category= → 供「住宿含 mini bar 商品」「套餐含特产伴手礼」等产品渲染商品清单GET /merchant-console/retail/products?status=onsale → 供住宿/餐饮页选品绑定(同一接口,B端带商户身份自动收窄)若后续住宿/餐饮需要在自己域内固化「本房型配哪些零售商品」,再由调用方建绑定表(如 room_retail_plans),零售侧零改动。这样耦合方向始终单向。
console_router = /api/miniapp/merchant-console/retail鉴权:_staff(pool, user) —— 查 merchant_staff 中 status='active' 行取 merchant_id,非商户成员 403(与 hotel/restaurant 完全一致,老板/员工两级均放行)。
商品
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /products | 列表;过滤 status/category/keyword,分页 limit/offset;出参 price/cost 为元、含 stock/stock_warn/low_stock 布尔 |
| POST | /products | 新增;name 必填否则 422 |
| PUT | /products/{id} | 编辑;非本商户 404 |
| DELETE | /products/{id} | 软删(status='offself'),保留历史订单引用 |
库存
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /products/{id}/stock | 入参 {change_type, qty, note};purchase/refund 为正、sale/adjust 可负;事务内 SELECT ... FOR UPDATE 锁行 → 校验 after_stock ≥ 0(不足 409)→ 更新 stock → 写 retail_stock_logs;库存归零自动 status='soldout' |
| GET | /products/{id}/stock-logs | 单品流水,分页 |
| GET | /stock-logs | 全商户流水,分页 |
订单
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /orders | 列表;过滤 status、days(默认 7)、分页 |
| GET | /orders/{id} | 详情含 items |
| POST | /orders | 开单;items 为空 422;事务内逐品校验在售 + 扣库存(不足 409 整单回滚)+ 写明细 + 算 total_fen + 写 sale 流水 |
| PUT | /orders/{id}/status | 改状态;paid/cancelled/refunded 校验;cancelled/refunded 回滚库存并写 cancel/refund 流水;已终态不可再改(409) |
| POST | /orders/{id}/settle | 结账;入参 pay_method、可选 paid_fen;paid_fen > total_fen → 422;写 paid_fen/paid_at/status='paid' |
概览
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /dashboard | today_orders(今日 paid 单量)、today_revenue(今日 paid_fen 合计)、sku_total、sku_onsale、low_stock_count(stock ≤ stock_warn AND status<>'offself')、open_orders(未结账) |
miniapp_router = /api/miniapp| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /retail/products | 只读商品目录;merchant_id 必填(缺失 422);商户不存在 404;仅返 status='onsale';不暴露 cost_fen(成本是商户机密) |
400 参数格式 / 401 未认证 / 403 非商户成员 / 404 资源不存在或跨商户 / 409 状态冲突·库存不足 / 422 业务校验失败。零 mock、零占位——库存不足即 409,不做「假装成功」。
ProductEdit 原选项为 餐饮/住宿/交通/门票/导游 —— 属旅行套餐语境残留,与零售 SKU 无关,本次替换为:
| value | label |
|---|---|
specialty | 特产 |
food | 食品 |
drink | 饮品 |
craft | 文创手作 |
daily | 日用 |
other | 其他 |
category字段为VARCHAR(32)自由文本(与餐饮menu_items.category一致),不做 DB 层 CHECK 约束 —— 便于商户自定义品类,前端用上述枚举做选择器默认值。
SELECT stock FROM retail_products WHERE id=$1 AND merchant_id=$2 FOR UPDATE,否则并发开单会超卖。raise HTTPException(409),事务上下文自动回滚已写入的其他明细与流水。after_stock = before_stock + qty_change,且 CHECK (after_stock >= 0),事后可用 SQL 全量对账。paid/cancelled/refunded 之间仅允许 paid → refunded(退款),其余转移 409。_fen;接口出参元、入参元(内部 *100)。paid_fen 入参保留分单位(与餐饮 settle 一致)。main.py 注册 retail.console_router / retail.miniapp_routerservices/retail.ts + retail.css(rl- 前缀独立)business-pack.ts PACK_EXTRA_ROUTES.retail 增补 3 路由(原 4 保留)Workbench.tsx 补零售 4 项 + 餐饮 4 项(餐饮上线时漏挂菜单,一并补齐)tsc 零错误(已反证)+ vite build 成功;prod 已部署(备份→解包 html/bworkbench/→清 AppleDouble→assets 同步根级→nginx -t+reload;b.kailashi.cn 与 doc.kailashi.cn/workbench/ 均 200,bundle 含零售/餐饮新入口字符串)/supplier-apply 与 /supplier-questionnaire 的去留:二者是入驻前页面,出现在商户工作台属语义错位。建议从 retail 包移出、归入 C端游客侧(后端 miniapp_merchant_applications.py 已就绪,只差 C端页面接线)。是否照此调整门禁?orders/支付链路,属独立工作量,建议另开任务。barcode 字段已预留(部分唯一索引),但 B端扫码枪/H5 摄像头扫码未做。是否需要 v1.1 补?零售包冒烟时意外暴露一个全站性缺陷,已修复并 staging 验证:
hotel/restaurant/retail/feature_entitlements/auth 五处各自写:
SELECT merchant_id, role FROM merchant_staff WHERE user_id=$1 AND status='active' LIMIT 1
均无 ORDER BY。PostgreSQL 对无序 LIMIT 返回行不保证稳定 → 同一用户不同请求可能落到不同商户 ⇒ 房型/菜品/商品/工作台凭证串户写入。
user_id=1 同时是 3 家真实商户 owner(冈仁波齐城堡酒店 / 芝达布日温泉 / 岗莎牦牛运输队);另 10 个测试用户各挂 3 商户。backend/app/merchant_scope.py:收拢为单一 resolve_merchant/require_staff,按 merchant_id ASC 确定性取值(止血:同一用户永远落同一商户)。is_test 列此前从未被写入(餐饮/酒店同样缺口)→ C端 WHERE is_test=FALSE 过滤形同虚设、清理测试数据无据。零售域全部 5 处 INSERT 补齐 is_test 继承(订单头继承商户、明细/流水继承商品)。staging 反证:is_test=t 时 C端查不到,临时改 f 后查得到且不泄 cost/stock。
tasks_full/admin/merchant_console/quotes(×2) 五处同类无 ORDER BY fetchrow 属「渔佳新媒体」名下代码,本次不越域改。is_test 写入缺口未在本次修。079 workbench_tickets 已带 merchant_id、auth.py 签发时也写入,但全仓零消费点;B端 无「当前商户」概念、无商户切换 UI。正解是 B端 请求带 X-Merchant-Id(或消费 ticket),后端校验成员身份后采用——涉及前端接线,跨域,需刘总令。