面向白名单运营商与持有方商家:自助完成创建、调价、户型维护、逐晚房态、上下架,并接住客户订单(查单 / 确认 / 拒单);C 端目录只展示 online 房源。鉴权复用平台商家开放接口同一套 HMAC-SHA256 机制(与家政开放接口同签名算法、同密钥体系)。
房源开放接口是平台商家开放接口的房源分支:路径前缀 /api/juzhu/housing/vendor/*,全部为 POST,请求/响应均为 JSON。鉴权、密钥、错误码风格与家政开放接口(/api/juzhu/jiazheng/vendor/*)完全一致,一套密钥两个业务共用。
status ∈ draft(草稿,默认)/ online(上架)/ offline(下架);C 端目录只出 online。目录有 ≤ 15 秒缓存,上下架最迟 15 秒生效。vendor_id + hmac_key。projects/list(只读)。projects/create 建草稿(默认不入 C 端),补户型与价格。projects/detail。projects/status 上架;日常用房态接口做关房/夜价,随用随调。| 环境 | Base | 说明 |
|---|---|---|
| 联调/演示 | https://sytest.meizu.life(本页同源,调试台直接可用) | 演示库,数据可随时清理重建 |
| 生产 | 以平台接入单下发的域名为准 | 同一套接口与签名,仅换 Base |
/api/juzhu/housing/vendor/* 即可;跨域接入方按上表 Base 拼全路径。
每个商家一个 vendor_id + 一把 hmac_key,由平台统一配置并线下下发。hmac_key 等同密码:只放服务端或自己的联调机,不要写进前端代码、不要提交仓库。
body,加上 vendor_id;剔除 sign 字段。父键.子键 点号展平;null、''(空串)的键不参与签名。true→'True'、false→'False');数组 → ['a', 'b'](单引号、逗号空格分隔)。k1=v1&k2=v2&…(& 连接,无转义)。timestamp(毫秒时间戳)参与上述串;用 hmac_key 做 HMAC-SHA256,取 hex 小写 为 sign。vendor_id + timestamp + sign,POST JSON 发送。时间戳与服务器偏差 ±5 分钟 内有效。// Node 签名示例
const crypto = require('crypto');
function pyStr(v) {
if (v === true) return 'True';
if (v === false) return 'False';
if (Array.isArray(v)) return '[' + v.map(x => (typeof x === 'string' ? "'" + x + "'" : pyStr(x))).join(', ') + ']';
return String(v);
}
function flatten(data, prefix) {
const flat = {};
for (const [k, v] of Object.entries(data || {})) {
if (v == null || v === '') continue; // 空值不参与签名
const key = prefix ? prefix + '.' + k : k;
if (v && typeof v === 'object' && !Array.isArray(v)) Object.assign(flat, flatten(v, key));
else flat[key] = pyStr(v);
}
return flat;
}
function sign(secretKey, body) {
const payload = { ...body };
delete payload.sign;
const timestamp = Date.now();
const flat = flatten(payload);
flat.timestamp = String(timestamp);
const stringToSign = Object.keys(flat).sort().map(k => k + '=' + flat[k]).join('&');
payload.timestamp = timestamp;
payload.sign = crypto.createHmac('sha256', secretKey).update(stringToSign, 'utf8').digest('hex');
return payload; // 直接作为 POST body
}
const body = sign(HMAC_KEY, { vendor_id: VENDOR_ID });
await fetch(BASE + '/api/juzhu/housing/vendor/projects/list', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
tags=['演示', '回归']),与 JSON 序列化不同;② 嵌套对象(如 units 数组里的对象)会被点号展平进签名,直接把最终 body 交给 sign 函数即可,不要手工拼;③ 重试时须重新签名(timestamp 会过期)。
响应统一为 JSON:成功 { "code": 0, "message": "success", … };失败 HTTP 状态码 + { "code": 4xx, "message": "原因" }(见 错误码)。
字段与平台房源模型一致、即接口契约本身;带 ★ 为商家写入接口接受的字段。
| 字段 | 类型 | 说明 |
|---|---|---|
id | int | 平台生成,创建后返回;后续接口用 id 定位房源 |
name | string | ★必填 | 房源/项目名
channel | string | ★ 业务频道:rental 租赁住宿(长租+旅居,监管同口径)/ minsu 民宿民宿 / newhouse 新房 / resale 二手 / trade 卖旧买新;缺省 rental,创建后不可改 |
city_id | int | ★必填 | 城市 id(
district_id | int | ★ 行政区 id(districts.id),须属于 city_id |
address | string | ★ 详细地址;缺省自动拼「城市 · 名称」 |
slug | string | URL 标识;缺省由名称自动生成并保证同频道唯一 |
cover_image | string | ★ 封面图 URL |
tags | string[] | ★ 标签数组(如 ["保租房","近地铁"]) |
price_from | int | ★ 起价,单位元。rental 口径为「元/月」,minsu 为「元/晚」;上架前置条件 |
sort_order / is_featured / old_house_hint | — | ★ 排序权重 / 平台精选(一般由平台设置)/ 卖旧买新提示 |
contact_phone | string | ★ 真实联系电话,仅入库用于虚拟号绑定,任何查询接口不回显 |
owner_vendor_id | int | 数据归属商家(签名商家自动写入,不可改) |
status | string | ★ draft 草稿(默认)/ online 上架 / offline 下架 |
ext | object | ★ 频道差异属性:insurance(保险标识数组,见 7)+ min_stay_nights(最短连住,见 6)+ stay_bookable(按晚预订开关,见 5.3) |
rating_status / rating | — | 评级状态与结果(平台侧维护,商家只读) |
| 字段 | 类型 | 说明 |
|---|---|---|
project_id | int | ★必填 | 所属房源 id
name | string | ★必填 | 户型名(如「两居 68㎡」「整栋 · 6 室」)
area_sqm | number | ★ 面积(㎡) |
layout_label | string | ★ 户型标签(2室1厅) |
rent_monthly | int | ★ 月租(元/月);rental 频道夜价按 月租/30 折算 |
price_total | int | ★ 总价(元),newhouse/resale 频道用 |
promo_price | int | ★ 优惠价(元) |
price_night | int | ★ 夜价(元/晚),写入 units.ext.price_night;minsu 频道预订按它计价 |
cancel_policy | object | ★ 免费取消政策(房型级),写入 units.ext.cancel_policy:{enabled, days_before, cutoff_time};免费取消窗口 = 入住日往前推 days_before(0-30,0=入住当天)天的 cutoff_time(HH:mm)时刻,窗口外 / 未开通一律不可取消不可退 |
tags / amenities / unit_spec / cover_image / sort_order | — | ★ 标签 / 配套枚举数组 / 房型说明 / 封面 / 排序 |
| 字段 | 说明 |
|---|---|
project_id + unit_id | 库存归属;unit_id=0 表示项目级(整栋 / 不限房型) |
stay_date | 晚(YYYY-MM-DD,入住当晚) |
status | open 可订(默认,未设置过的晚即可订)/ blocked 关房(商家)/ booked 已订(平台订单写入,接口不可改,取消自动释放) |
price_night | 当晚价格覆盖(元),未覆盖回落默认夜价;户型级 > 项目级 > 默认 |
GET /api/juzhu/projects/{id}/stay-calendar?month=YYYY-MM&unit_id=(公开只读,用于上下架 / 房态验收)返回逐晚三态 + 价格 + 最短连住 + 保险标识,与 C 端日历渲染一致。
以下示例省略签名三件套(vendor_id / timestamp / sign),实际每个请求体都必须携带(见 §2,或直接用 §7 调试台)。
{
"channel": "rental", // 可选过滤:channel / status / city_id / keyword
"status": "online"
}
→ 200
{
"code": 0, "message": "success",
"list": [
{
"id": 1611, "name": "示例·观山遇见旅居", "channel": "rental",
"status": "online", "price_from": 2400,
"min_stay_nights": 15,
"insurance": ["switch_rental", "property"],
"insurance_types": [ { "key": "switch_rental", "label": "换租保险", "icon": "🔄" }, /*…*/ ]
}
],
"total": 1
}
{ "id": 1611 }
→ 200 { "code": 0, "project": { /* 同 4.1,另含 address/tags/ext 等 */ }, "units": [ /* 户型数组 */ ] }
{
"name": "观山湖·遇见旅居",
"channel": "rental",
"city_id": 4,
"district_id": 41,
"address": "观山湖区 · 示例大道 88 号",
"tags": ["旅居", "近地铁"],
"price_from": 2400,
"contact_phone": "13800001234", // 仅入库(虚拟号绑定),永不回显
"min_stay_nights": 15, // 最短连住,写入 ext(见 §5)
"insurance": ["switch_rental", "property"], // 保险标识(见 §6)
"units": [
{ "name": "一居 45㎡", "layout_label": "1室1厅", "area_sqm": 45, "rent_monthly": 2400 }
],
"status": "draft" // 缺省 draft;draft/offline 不出 C 端
}
→ 200 { "code": 0, "message": "success", "project": { "id": 1611, "status": "draft", /*…*/ } }
{
"id": 1611,
"price_from": 2680,
"tags": ["旅居", "近地铁", "含早"],
"insurance": ["hotel_cancel", "property"],
"min_stay_nights": 20,
"stay_bookable": true
}
→ 200 { "code": 0, "project": { /* 更新后全量 */ } }
不可改:city_id、channel、owner_vendor_id;改 name 会同步重生成唯一 slug。
{ "id": 1611, "status": "online" } // online 上架 / offline 下架 / draft 转草稿
→ 200 { "code": 0, "message": "success", "id": 1611, "status": "online" }
price_from 且至少有 1 个户型,否则 400。C 端目录有 ≤ 15 秒缓存,上架后最迟 15 秒可见、下架后最迟 15 秒消失。
{ "project_id": 1611, "name": "两居 68㎡", "layout_label": "2室1厅",
"area_sqm": 68, "rent_monthly": 3200 }
→ 200 { "code": 0, "unit": { "id": 3101, /*…*/ } }
{ "id": 3101, "rent_monthly": 3380, "price_night": 128,
"cancel_policy": { "enabled": true, "days_before": 1, "cutoff_time": "18:00" } }
→ 200 { "code": 0, "unit": { /*…*/ } }
cancel_policy(非法 days_before/cutoff_time 返回 400);传 null 清除(= 未开通,客户不可取消)。免费取消窗口 = 入住日往前推 days_before 天的 cutoff_time 时刻;客户在窗口内取消自动退款并释放房态。{ "id": 3101 }
→ 200 { "code": 0, "message": "success" }
{ "project_id": 1611, "unit_id": 0, "month": "2026-12" }
→ 200 {
"code": 0, "month": "2026-12", "base_price_night": 80,
"min_stay_nights": 15, "insurance": ["switch_rental", "property"],
"days": [
{ "date": "2026-12-19", "status": "booked", "price": 80, "source": "booking", "booking_id": 36 },
{ "date": "2026-12-20", "status": "open", "price": 80 }
]
}
客户在 C 端提交预订单后,商家经开放接口完成接单闭环:定时拉取订单 → 确认生效或拒单 → 拒单自动释放房态。订单按签约商家隔离,联系人手机号只回掩码(139****5678)。
{
"status": "pending", // 可选过滤:status / pay_status / project_id
"project_id": 1611
}
→ 200 { "code": 0, "list": [ {
"id": 36, "order_no": "BKG-RENTAL-00036", "project_name": "观山湖·遇见旅居",
"checkin": "2026-12-19", "checkout": "2027-01-08", "nights": 20,
"price_total": 1600, "commission_rate": 8.00, "commission_fee": 128.00,
"status": "pending", "pay_status": null,
"contact_name": "张**", "contact_phone": "139****5678"
} ] }
commission_rate / commission_fee 为下单时锁定的费率与佣金快照(元),结算按快照执行、商家后续费率调整不追溯;未差异化商家按全局基准(缺省 10%)。{ "id": 36 } → 200 { "code": 0, "booking": { /* 同上,单条 */ } }
{ "id": 36 } → 200 { "code": 0, "status": "confirmed" }
{ "id": 36 }
→ 200 { "code": 0, "status": "cancelled", "pay_status": null }
| 字段 | 取值 | 说明 |
|---|---|---|
status | pending → confirmed / cancelled | 确认后订单生效;取消为终态,不可再变更 |
pay_status | unpaid / paid / refunded 或 空 | 民宿民宿为预付(先付后确认);租赁住宿预订单为空(商家确认后线下收款) |
| 确认前置 | — | unpaid 的订单不可确认(400),支付完成后可确认;cancelled 不可再变更 |
| 拒单/取消 | — | 自动释放占用的房态晚;已支付订单 pay_status 标记 refunded |
客户动作触发的订单变更,平台会主动推送签名事件到你为商家配置的通知地址(接入单里提供 URL 即可),不必轮询等单:
| event | 触发时机 | 商家应做什么 |
|---|---|---|
booking.created | 客户提交预订单 | 备房;预付单等待支付到账 |
booking.paid | 预付单支付完成(民宿民宿) | 此时方可确认订单 |
booking.cancelled | 客户取消(商家自己拒单不会推送) | 释放已备房源 |
推送为 POST JSON,body 即开放接口同款结构:event + vendor_id + order(订单对象,手机号不外传)+ timestamp + sign。验签与开放接口同一算法(对去掉 sign/timestamp 的参数扁平排序后 HMAC-SHA256),收到后先验签再处理:
// Node 验签示例(与平台 hmac_auth.cjs 同算法)
app.post('/hooks/booking', (req, res) => {
const body = req.body;
const payload = { ...body };
delete payload.sign;
const flat = flatten(payload); // 与请求签名同一套 flatten
flat.timestamp = String(body.timestamp);
const expect = crypto.createHmac('sha256', HMAC_KEY)
.update(Object.keys(flat).sort().map(k => k + '=' + flat[k]).join('&')).digest('hex');
if (expect !== String(body.sign).toLowerCase()) return res.status(401).end();
// 处理 body.event / body.order …
res.status(200).end('ok'); // 2xx 即视为送达
});
bookings/list 定时对账兜底(推荐粒度:每分钟拉一次 status='pending')。订单明细里不含客户明文手机号。
{
"project_id": 1611,
"unit_id": 0, // 0 或缺省 = 项目级(整栋 / 不限房型)
"dates": ["2026-12-10", "2026-12-11"],
"status": "blocked", // blocked 关房;open 开房
"price_night": null // blocked 可同时设价;open + 价格 = 夜价覆盖;open + null = 恢复默认
}
→ 200 { "code": 0, "affected": 2, "dates": 2 }
booked(订单占用)返回 400 并列出日期,须先取消订单。旅居/短住按连续入住售卖:平台缺省口径为 rental=15 晚、minsu=1 晚,商家可用 ext.min_stay_nights(1–365)按房源覆盖。三处同口径校验:C 端日历选段、下单页、平台下单接口 POST /api/juzhu/booking(不足返回 400「该房源须连住至少 N 晚」)。下单成功即占用这些晚,取消自动释放。
房源默认仅 400 电话咨询(C 端不展示价格日历、下单接口拒绝)。projects/create / projects/update 传 "stay_bookable": true 开通后,C 端才展示日历并支持在线下单(catalog / 详情 / 房态日历出参均带 bookable 能力位)。关闭传 false(已生成订单不受影响,可正常确认/取消)。判断由平台服务端唯一执行,tag / 频道不参与。
商家为房源配置的「入住保障」,随 catalog / 详情 / 房态日历下发 C 端展示。枚举由平台统一维护,接口只接受以下 key,未知 key 返回 400:
| key | 名称 | 保障口径 |
|---|---|---|
switch_rental | 换租保险 | 租期到期换租衔接期内的房租损失与搬迁费用 |
hotel_cancel | 酒店取消险 | 行程变更按规则退改,未入住部分房费可退 |
property | 财产保险 | 入住期间屋内财产与房屋主体意外损坏 |
配置入口:projects/create / projects/update 传 insurance 数组;清空传 []。出参同时给出 insurance(key 数组)与 insurance_types(含 label/icon,可直接渲染)。
在本页直接签名并调用真实接口:选择接口 → 填业务参数 JSON(无需自己写 vendor_id/timestamp/sign)→ 发送。签名在你的浏览器本地完成,hmac_key 只留在本机(可选「记住」,存 localStorage,换机不带)。
常用模板:上架 {"id":123,"status":"online"} · 关房 {"project_id":123,"dates":["2026-12-10"],"status":"blocked"} · 建房源见 §4.3 示例 · 接单 {"id":36}(confirm/cancel 同参,id 取 bookings/list 返回值)。
| HTTP | code | 含义 | 处置 |
|---|---|---|---|
| 401 | — | 缺失签名(sign)或时间戳(timestamp)参数 | 检查签名三件套是否都在 body 里 |
| 401 | — | 请求已过期(±5 分钟窗口) | 校准服务器时间后重签 |
| 401 | — | 签名校验失败 / vendor_id 密钥未配置 | 核对 hmac_key 与签名算法(数组、空值、嵌套展平) |
| 400 | — | name / city_id / project_id 等必填缺失 | 按 message 补齐 |
| 400 | — | channel 非法 / city_id 不属于商家开放城市 / district_id 不属于城市 | 先用 projects/list 或平台侧确认主数据 |
| 400 | — | 上架被拒:缺 price_from 或无户型 | 补价 + units/create 后重试 |
| 400 | — | insurance 含未知标识 / min_stay_nights 不在 1–365 | 按 §6 / §5 枚举修正 |
| 400 | — | 房态:以下日期已有预订占用 | 先取消订单再改房态 |
| 400 | — | 无可更新字段 / city_id 不支持修改 | 按 message 调整 |
| 400 | — | 订单未支付不可确认 / 订单已取消不可变更 | 支付走收银台后重试确认;取消为终态 |
| 404 | 404 | 订单不存在或不属于该商家(房源 / 户型同理) | 统一 404,不区分「不存在」与「越权」,防探测 |
| 404 | 404 | 未知 vendor 路由 | 核对路径(本页 8 条) |
| 405 | — | 非 POST 请求 | 全部接口仅支持 POST |
平台提供端到端回归脚本(Node,走真实 HTTP 签名链路),接入方可用它自检签名实现是否正确:
# 覆盖:创建→上架→C 端可见→保险/连住更新→房态→下架→越权负例→清理
node scripts/housing_vendor_hmac_regression.cjs [base_url] # 默认 http://127.0.0.1:8766
node scripts/vendor_hmac_regression.cjs # 家政开放接口同机制回归
版本:v2.0(2026-09)· 文档与线上实现同步校准。
密钥与凭证:由平台统一配置并线下下发;遗失须走平台重置,不要在多个业务间共享 hmac_key。
试点阶段接口字段以本页为准,扩展字段只增不破坏;重大变更会提前在「接入单」里同步。发布节奏随政策与试点进展而定。