🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 外卖接口(前端) ## 1. 简述 ### 1.1 常规说明 * 接口使用 RESTful 风格。 * URL中的 `<id>` / `<slug>` 替换为实际的数值。 * **状态码**一般都是指 HTTP 状态码。 * 身份验证信息放在 `Authorization` 请求头,示例 `Authorization: Bearer <token>` * 带有 **公开** 字样的接口是可以匿名访问的,为了便于封装,所有接口都可以带上身份验证信息,只是公开的接口会忽略身份验证信息 ### 1.2 HTTP 状态码及返回内容说明 **HTTP 状态码说明:** * 200 - 请求成功 * 201 - 请求成功, 并创建了一个对象. 一般是执行 **POST** 动作创建一个新对象时返回. * 204 - 请求成功, 没有内容返回. 一般是执行 **DELETE** 动作时返回. * 400 - 客户端请求无效. 一般是指客户端提交的参数不符合要求. * 401 - 需要身份验证. 当需要身份验证, 而未提供身份验证信息或者身份验证信息无效. * 403 - 禁止访问. 一般情况是权限不足. * 404 - 资源不存在 * 405 - 动作不允许. 例如接口只接受 **POST** 动作, 而客户端使用了 **GET** 动作. * 500 - 服务器内部错误 **返回内容说明:** 返回内容是 JSON 格式。除了 2xx, 400 状态码之外, 其他状态码的出错信息都出现在 `detail` 字段中, 例如: ``` { "detail": "身份认证信息未提供。" } ``` **400状态码返回内容说明:** 客户端请求参数检查不通过的话,返回状态码都是 400,参数出错的话,返回结果中带有对应参数的出错信息列表(数组),一般情况下数组只有一个元素,对于不是参数出错的错误信息,通过 `non_field_errors` 给出,格式与参数的出错信息相同(都是数组)。 例如: ``` { "context": [ "使用场景(context)不能为空" ] } ``` ### 1.3 国际手机号码说明 1. 国内手机号码不存储**国家/地区代码**,仅存储 11 位数字,如:`18682265887` 2. 国际手机号码存储**国家/地区代码**(包含前置的加号(`+`))和**手机号码**,如:`+79118064728` 3. 客户端提交手机号码参数时,将**国家/地区代码**和**手机号码**组合成一个字符串,如:`+79118064728`,`+8618682265887`。后端接收到之后自动进行处理,对于`+86`开头的手机号码,自动去掉`+86`标识。 ### 1.4 桌台二维码说明 桌台二维码分为 3 类:堂食桌台、外卖桌台、等取桌台。 二维码 URL 规则 * 堂食 `http://xxx.com/?rid=1&tid=2&ver=1` * 堂食(自选) `http://xxx.com/?rid=1&tid=-99&ver=1` * 外卖 `http://xxx.com/?rid=1&tid=-1&ver=1` * 等取 `http://xxx.com/?rid=1&tid=-2&ver=1` 查询参数说明: * `rid` - 表示外卖后端的餐厅id * `tid` - 表示桌台id(以收银端桌台table_id为准) * `ver` - 表示二维码版本 扫描二维码进入手机外卖前端页面(H5)之后,前端从外卖后端获取到对应餐厅的桌台数据(在餐厅信息中),并对比 URL 传过来的 `ver` 是否与后端的(`qrcode_version`)一致,若不一致,则可以告知二维码失效,并禁用点餐功能。 ## 2. 餐厅接口 ### 2.1 餐厅信息 `GET /api/v1/frontend/restaurants/<id>?table_id=<table_id>` 查询参数: * `table_id` - 桌台ID(二维码中的`tid`),`paid_types` 数据与桌台(用餐类型)有关 返回数据结构如下: ``` { "id": 1, // 餐厅id "categories": [ // 分类 { "id": 2, // 分类id "name": "Esppresso" // 分类名称 }, { "id": 4, "name": "Brewed" }, { "id": 6, "name": "Hot" }, { "id": 8, "name": "Iced" }, { "id": 10, "name": "Blended Ice" }, { "id": 12, "name": "Food Items" } ], "takeout": { // 外卖信息 "slides": [ // 餐厅图片列表 { "image": "http://www.baidu.com", "link": "http://gicater.com", }, { "image": "http://www.baidu.com", // 图片URL "link": "", // 链接URL,值为空则不跳转 } ], "phones": [ // 餐厅电话列表 "0755-81234567" ], "eat_types_enable": [ // 用餐类型状态(是否可接单),以 eat_type 值作为索引(下标) true, // 下标0,堂食是否接单,true-是,false-否 true, // 下标1,外卖(配送)是否接单,true-是,false-否 false, // 下标2,外卖(自取/来取)是否接单,true-是,false-否 true // 下标3,等取是否接单,true-是,false-否 ], "cny_exrate": 0.0, // 餐厅计价货币对人民币汇率,用于在线支付时转换成人民币金额;人民币金额=餐厅计价货币金额*cny_exrate,只有 cny_exrate 大于0时进行换算,否则 人民币金额=餐厅计价货币金额。 "address": "", // 餐厅地址 "is_user_address_reversed": false, // 用户地址是否反向,true-反向(从小到大),false-不反向(从大到小) "region": "深圳市", // 餐厅所在城市,只有中文版收银有值,可用在百度地图接口 region 字段 "delivery_times": [ // 外卖配送可选的时间列表(单位分钟),超过60分钟的,前端自行转换成时分格式 30, 45, 60, 75, 90, 105, 120, 135 ], "collect_times": [ // 外卖自取可选的时间列表(单位分钟) 15, 30, 45, 60, 75, 90 ], "longitude": 0.0, // 餐厅经度 "latitude": 0.0, // 餐厅纬度 "notice": "公告", // 公告 "summary": "餐厅简介", // 简介 "attention": "注意事项", // 注意事项 "show_total": 1, // 是否显示总价 "show_zero": 0, // 是否显示价格为0的菜 "enable_service": 1, "pay_mch": "qf", // 支付服务商标识,qf-钱方,sqb-收钱吧。钱方需要走获取用户 openid 流程,参考 8 钱方微信支付 "language_type": 1, // 语言类型,1-系统默认,2-自定义(可自选) "country_code": "+86", // 国家地区代码,发送国际短信时需要 "takeaway_use_takeout_price": true // 等取是否使用外志价格,true-是,false-否 }, "takeout_delivery_fee": { // 外卖费信息 "delivery_scope": "0.0~20.0", // 配送范围,单位公里,只做显示,不做计算依据 "min_amount": 5.0 // 起送金额 }, "takeout_periods": { // 外卖营业时间,0周日,1-6周一至周六,每天有多个时间段(目前最多2个) "open": [ // 是否营业,true-营业,false-停业;下标 0~6 表示周日至周六 false, false, true, true, true, true, true ], "0": [ { // 营业时间段 "start_time": "12:00", // 开始时间 "end_time": "14:00" // 结束时间 } ], "1": [ { "start_time": "00:00", "end_time": "23:59" } ], "2": [ { "start_time": "00:00", "end_time": "23:59" } ], "3": [ { "start_time": "00:00", "end_time": "23:59" } ], "4": [ { "start_time": "00:00", "end_time": "23:59" } ], "5": [ { "start_time": "00:00", "end_time": "23:59" } ], "6": [ { "start_time": "00:00", "end_time": "23:59" } ] }, "tables": [ // 桌台列表 { "table_id": 1, // 桌台ID "table_name": "A1", // 桌台名称 "seat_num": 1, // 座位数 "qrcode_version": 1 // 桌台二维码版本号 }, { "table_id": 2, "table_name": "A2", "seat_num": 1, "qrcode_version": 1 }, { "table_id": 5, "table_name": "A5", "seat_num": 1, "qrcode_version": 1 } ], "paid_types": [ // 可用的支付类型列表(下单接口会用到) { // 默认线下支付 "value": 1, "label": "线下支付" }, { // 微信支付需要餐厅支持,餐厅支持的情况下,前端需要判断是不是微信浏览器,若不是,则不可用 "value": 2, "label": "微信支付" }, { // 需要餐厅支持并启用 "value": 3, "label": "会员卡" } ], "order_default": { // 开台消费,没有的话返回 null "periods": [ // 有开台消费的时间段,如果开始时间大于结束时间,比较方法 start_time <= nowTime 或者 end_time >= nowTime { "start_time": "04:00", // 开始时间 "end_time": "14:00" // 结束时间 }, { "start_time": "14:00", "end_time": "04:00" } ], "products": [ // 开台消费必选的商品(菜品) { "id": 480, // 商品id "item_id": 6005, // 商品item_id "item_name": "Garden Salad", // 商品名称 "item_type": 0, "specs": [ // 规格,只有1个 { "pu": 0, // 规格索引 "price": 4.2, // 价格(堂食) "unit": "", // 规格名称(单位) "takeout_price": 4.2 } ], "thumbnail": "//pos-cn-node.oss-cn-beijing.aliyuncs.com/1.jpg", // 缩略图URL "description": "Refreshing ingredient with savory Thousand Island Sauce", "num": 1, // 单份包含商品的数量 "is_by_seat": false // 是否按位消费 } ] }, "name": "aaa", // 餐厅名称 "location_name_1": "", // 餐厅地址 "slug": "", "is_support_tax": false, // 是否支持计税功能,若不支持,下单时可不调用税费计算接口 "show_privacy_policy": false, // 是否显示隐私政策,true-显示,false-不显示 "product_include_tax": true, // 菜品是否含税 "currency_name": "¥", // 货币符号 "decimal_places": 2, "decimal_char": ".", "edition": "cn", // 收银软件版本,cn-中文版,en-英文版,优先判断是不是中文版,其他情况可当作英文版 "start_time": "04:00:00", "is_member_card_enable": true, // 会员卡服务是否可用,true-可用,false-不可用 "tz_offset": 0, "tz_string": "Z", "today": "2019-04-18", // 餐厅当前日期 "weekday": 4 // 餐厅当前周几,0周日,1-6周一至周六 } ``` 营业状态判断(至少要做 3 个判断): 1. 用餐类型(`eat_type`)是否**可接单**,根据 `takeout.eat_types_enable` 判断 2. 当天**是否营业**,根据 `takeout_periods.open` 判断 3. 当前时间是否在营业时间段内,根据 `takeout_periods.X` 判断 ## 3. 用户接口 ### 3.1 发送短信验证码 `POST /api/v1/frontend/users/send-phone-token` 提交参数: * `res_id` - 餐厅id * `phone` - 手机号码 * `context` - 短信场景,`login`-登录(验证码快捷登录),`register`-注册/绑定手机 请求成功返回状态码200,返回数据结构如下: ``` { "send": true // true 表示发送成功, false 表示发送失败 } ``` ### 3.2 登录 `POST /api/v1/frontend/users/login` 提交参数: * `res_id` - 餐厅id * `username` - 手机号码 / 电子邮件,目前只支持手机号码 * `password` - 密码,至少 6 位;若使用手机短信快捷登录,这里传手机验证码,短信发送接口 `context=login` 身份验证成功的话返回状态码200,返回数据结构如下: ``` { "id": 2, "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjIsImlhdCI6MTU0ODAzNjI2NCwiZXhwIjoxNTQ4MTIyNjY0fQ.thercE0EgPEWDtwF-YhSRUw8zu2UTOO0sBuaTI1yM_g", // token "ttl": 86400 // token 有效期(单位秒) } ``` 若用户名或密码错误,返回状态码403。 ### 3.3 获取用户信息 `GET /api/v1/frontend/users/me` ``` { "id": 1, "username": "digwtx01", // 用户名 "phone": "", // 电话号码 "email": "", "avatar": "http://www", // 头像URL "nick_name": "digwtx", // 昵称 "addresses": [{ // 送餐地址列表 "id": 1, "name": "iwerwer", // 姓名 "gender": 0, // 性别,0-女性,1-男性 "phone": "15389781522", // 电话 "address": "iwierew", // 地址 "distance": 2, // 与餐厅的距离(单位千米) "is_default": false // 是否为默认地址 }, { "id": 2, "name": "iwerwer", "gender": 0, // 性别,0-女性,1-男性 "phone": "18682265887", "address": "iwierew", "distance": 3, "is_default": false } ], "member_card": { // 会员卡信息,若没有则为 null "card_id": "wx001" // 会员卡号 } } ``` ### 3.4 修改用户信息 `POST /api/v1/frontend/users/me` 可修改的信息(可任意修改一项或多项): * `avatar` - 头像URL * `nick_name` - 昵称 修改成功返回状态码200,数据数据结构与 **3.3 获取用户信息** 相同。 ### 3.5 Facebook 登录 假设前端使用 Facebook JavaScript JDK 进行开发,可以获取到用户的 `access_token`。 当拿到 FB 用户的 `access_token` 之后,调用 `3.2 登录` 接口进行登录。 提交参数(JSON): ``` { "res_id": 1, // 餐厅id,按实际传值 "username": "user_id", // 可随意传,只要不为空即可 "password": "access_token", // FB 用户的 access_token,非常重要 "user_type": 2 // 固定传2 } ``` 返回及异常参考 `3.2 登录` 接口。 ### 3.6 绑定手机号码 `POST /api/v1/frontend/users/bind-phone` 此接口需要提供身份信息。 此接口需要发送短信验证码:向需要绑定的手机号码发送一个注册验证码,短信发送接口 `context=register` 提交参数示例(JSON): ``` { "phone": "13811115555", // 需要绑定的手机号码 "token": "138532" // 手机验证码 } ``` 绑定成功的话返回状态码200。 若验证码无效,则返回状态码400。 若用户已绑定手机号码,则返回状态码403,返回信息**用户已绑定手机号码**。 ## 4. 用户地址接口 此部分接口需要登录。 ### 4.1 地址列表 `GET /api/v1/frontend/user-addresses` 返回数据结构如下: ``` { "count": 3, "code": 0, "next": null, // 下一页URL "previous": null, // 上一页URL "results": [ // 结果集 { "id": 2, "name": "iewirwe", // 姓名 "gender": 0, // 性别,0-女性,1-男性 "phone": "irweirweiriwe", // 电话 "address": "iweirweir", // 地址 "longitude": 0, // 经度 "latitude": 0, // 纬度 "distance": 1, // 地址与餐厅的距离(单位千米) "is_default": true // 是否为默认地址 }, // ... ] } ``` ### 4.2 地址详情 `GET /api/v1/frontend/user-addresses/<id>` 数据结构参与列表接口。 ### 4.3 增加地址 `POST /api/v1/frontend/user-addresses` 提交数据结构(JSON): ``` { "name": "iewirwe", // 姓名 "gender": 0, // 性别,0-女性,1-男性 "phone": "15388882222", // 联系电话 "address": "iweirweir", // 地址 "longitude": 0, // 经度(可不传,建议传) "latitude": 0, // 纬度(可不传,建议传) "distance": 1, // 地址与餐厅的距离(单位千米),负数表示距离未知 "is_default": false // 是否为默认地址 } ``` ### 4.4 修改地址 * `PUT /api/v1/frontend/user-addresses/<id>` 要提交所有需要字段 * `PATCH /api/v1/frontend/user-addresses/<id>` 可只提交部分字段 ### 4.5 删除地址 `DELETE /api/v1/frontend/user-addresses/<id>` 删除成功的话返回状态码204。 ### 4.6 设为默认地址 `POST /api/v1/frontend/user-addresses/<id>/set-default` 设置成功的话返回状态码200,返回地址详情。 ## 5. 商品(菜品)接口 普通菜品、套餐、调味品本质上都是商品,都存储在同一个数据库表中,数据结构也大致相同。 1. 普通商品有**多规格**,套餐目前暂定单规格。多规格单选。 2. 普通商品有**可选调味品**,套餐本身的**可选调味品**无效,套餐内菜品有**可选调味品**。**可选调味品**多选。调味品有可能有**规格**(单位),单规格的。 3. 套餐**必选组**内的菜品全部必选。 3. 套餐**可选组**中的菜品可能有**多规格**,如果处理比较麻烦,当有多规格时,可考虑只使用第一规格。 ### 5.1 商品列表 `GET /api/v1/frontend/products/simple?res_id=<res_id>&table_id=<table_id>` 参数说明: * `res_id` - (必填)餐厅id * `table_id` - (必填)桌台ID(二维码中的`tid`),菜品跟用餐类型有关,有些菜品只在指定的用餐类型中可用 * `category_id` - (选填)分类id,分类id从餐厅信息 `categories` 中获取 返回数据结构如下(默认带分页,可针对需求取消分页): ``` { "count": 3, "next": null, // 下一页URL "previous": null, // 上一页URL "results": [] // 结果集,单个数据结构参见商品详情 } ``` ### 5.2 商品详情 `GET /api/v1/frontend/products/<id>?res_id=<res_id>` 查询参数说明: * `res_id` - (必填)餐厅id 返回数据结构如下: ``` { "id": 30, // 商品id "specs": [ // 规格,至少会有1个,如果只有一个,则不需要选择 { "pu": 0, // 规格索引,值范围0~4 "price": 5.0, // 价格(堂食) "takeout_price": 5.0, // 外卖价格 "unit": "Dozen" // 规格(单位) } ], "condiments": [ // 需要选择的调味品 { "id": 33, // 调味品(商品)id "item_id": 6002, // 调味品(商品)item_id "item_name": "no sugar", // 调味品(商品)名称 "price": 0.0 // 价格,有些调味味需要额外收费 }, { "id": 34, "item_id": 6003, "item_name": "cream", "price": 3.0 } ], "thumbnail": "", // 商品缩略图URL "images": [ { "url": "xxxx" } ], "tax": 0, "courses": [ // 套餐组信息,只有当商品是套餐时才会有 { "id": 1, // 分组id "group_name": "必选组", // 分组名称 "is_must": 1, // 是否必选,1-必选,0-可选,必选的话则该分组下所有商品都要选择 "choose_num": 1, // 需要选择的数量,必选组的话,则这个数量忽略 "entries": [ // 分组内的商品 { "id": 21, // 商品id "item_id": 1021, // 商品item_id "item_name": "Iced Caffe Mocha", // 套餐中的商品名称 "num": 1.0, // 包含数量 "specs": [{ // 套餐组中的规格,至少有一组规格,也可能有多组规格 "pu": 1, // 规格索引 "unit": "C", // 规格,显示时需要将规格一并显示 "price": 0, // 加价价格,大于0表示加价,如果选择了的话,则需要计入套餐总价 "origin_price": 4.0, // 原始价格,可用在某些需要显示的场景 }], "condiments": [] } ] }, { "id": 2, "group_name": "可选组", "is_must": 0, // 是否必选 "choose_num": 2, "entries": [ { "id": 17, "item_id": 1017, "item_name": "Bottle of Water", "num": 1.0, "specs": [{ "pu": 0, "unit": "C", "price": 1.0, "origin_price": 2.0 }], "condiments": [ // 套餐中的商品也是有可能需要选择调味品的 { "id": 33, "item_id": 6002, "item_name": "no sugar", "price": 0.0 }, { "id": 34, "item_id": 6003, "item_name": "cream", "price": 3.0 } ] }, { "id": 12, "item_id": 1012, "item_name": "Starbucks Coffee", "num": 1.0, "specs": [{ "pu": 0, "unit": "T", "price": 0.0, "origin_price": 2 }], "condiments": [] } ] }, { "id": 3, "group_name": "可选组2", "is_must": 0, "choose_num": 1, "entries": [ { "id": 13, "item_id": 1013, "item_name": "Shot In The Dark ", "num": 1, "specs": [ // 套餐组中的菜品的多个规格 { "pu": 0, "unit": "T", "price": 0, "origin_price": 3.25 }, { "pu": 1, "unit": "C", "price": 0, "origin_price": 3.5 }, { "pu": 2, "unit": "V", "price": 0, "origin_price": 3.75 } ], "condiments": [] }, { "id": 9, "item_id": 1009, "item_name": "Espresso", "num": 1.0, "specs": [ "pu": 0, "unit": "D", "price": 0.5, "origin_price": 3, ], "condiments": [] } ] } ], "item_id": 6004, // 商品item_id "item_name": "abc23ccccc", // 商品名称 "item_type": 3, // 商品类型,0-普通菜品,3-套餐 "box_fee": 0.0, // 餐盒费 "category": 8, "tax_group": null } ``` ## 6. 订单 ### 6.1 订单列表 * 外卖订单列表 `GET /api/v1/frontend/orders?res_id=<res_id>&table_id=-1` [需要登录] * 堂食订单列表 `GET /api/v1/frontend/orders?res_id=<res_id>&table_id=<tabel_id>` 查询参数: * `res_id` - (必填)餐厅id * `table_id` - (必填)桌台id * `status` - (选填)订单状态,`9`-已完成的,`unfinished`-未完成的 返回数据结构: ``` { "count": 15, "code": 0, "next": null, // 下一页URL "previous": null, // 上一页URL "results": [ // 结果集 { // 订单 "id": 1, // 订单id "products": [ // 订单产品(菜品) { "product_item_id": 6000, "product_item_name": "milk", // 菜品名称 "pu": 0, "unit": "C", // 菜品规格 "desc": "C", // 描述信息,如果是套餐,则描述套餐内容,如果是普通菜品,则描述规格 "price": 3.0, // 菜品单价 "origin_price": 0.0, "qty": 1, // 菜品数量 "box_fee": 0.0, // 单个菜品总的餐盒费 "total_price": 3.0 // 总价 }, { "product_item_id": 1016, "product_item_name": "Hot Chocolate", "pu": 0, "unit": "D", "desc": "D", "price": 25.0, "origin_price": 0.0, "qty": 1, "box_fee": 0.0, "total_price": 25.0 } ], "customer_name": "", // 客户姓名 "customer_phone": "", // 客户电话 "customer_address": "", // 客户地址 "customer_count": 1, // 客户人数或者餐具数 "eat_type": 0, // 0-堂食,1-外卖配送,2-外卖自取,3-等取 "paid_type": 1, // 付款方式,1-线下支付,2-微信支付,3-会员卡 "is_paid": false, // 是否支付,支付状态和订单状态分开 "paid_time": null, // 支付时间 "plan_time": null, // 计划时间(预计时间) "total_price": 29.0, // 菜品总价 "total_box_fee": 0.0, // 总餐盒费 "total_tax_fee": 0.0, "delivery_fee": 0.0, // 配送费 "total_fee": 35.0, // 订单总金额 "remark": "", // 备注信息 "trade_no": "11556070571001", // 订单号 "take_code": "15", // 取餐号,eat_type值为0、2、3时才会有 "take_code_datauri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARwAAABSCAIAA", // 取餐号条形码图片(DataURI,Base64格式) "status": 1, // 订单状态,0-用户下单,1-待商家确认,2-商家制作中,3-商家制作完成,4-配送中,5-已送达,9-已完成,-1-用户取消,-2-商家取消 "create_time": "2019-04-24T09:49:31.543714+08:00", // 下单时间 "restaurant": 1, "table": { // 如果没有会是 null "table_id": 1, "table_name": "1", } }, // ... ] } ``` ### 6.2 订单详情 `GET /api/v1/frontend/orders/<id>?res_id=<res_id>` 数据结构参考列表接口。 ### 6.3 计算配送费 `POST /api/v1/frontend/orders/calc-delivery-fee?res_id=<res_id>` 提交参数: ``` { "distance": 2 // 距离(单位千米) } ``` 返回数据结构: ``` { "delivery_fee": 0 // 配送费,负数表示不在配送范围 } ``` ### 6.4 计算税费 `POST /api/v1/frontend/orders/calc-tax-fee?res_id=<res_id>` 提交数据结构(为了方便,可直接提交**下单**接口的数据结构): ``` { "restaurant": 1, // res_id "delivery_fee": 0, // 配送费 "total_box_fee": 0, // 总餐盒费 "discount_fee": 0, // 折扣金额,正数 "products": [{ "product": 549, // 菜品id "price": 4.2, // 菜品价格 "qty": 1, // 数量 "item_id": 6005, // 菜品item_id "item_name": "Garden Salad", // 菜品名称 "total_price": 4.2, // 菜品总价 } ] } ``` 返回数据结构: ``` { "tax_fee": "1.06", // 税费 "is_tax_included": false // 是否已含税,若已含税,则税费不计入 total_fee } ``` ### 6.5 下单 `POST /api/v1/frontend/orders?res_id=<res_id>` 提交数据结构(JSON): ``` { "restaurant": 1, // 餐厅id "table_id": 5, // 桌台id,参考桌台信息 "eat_type": 1, // 0-堂食,1-外卖配送,2-外卖自取,3-等取 "paid_type": 1, // 支付类型,1-线下支付(默认),2-微信支付,3-会员卡 "user_address": 18, // 用户收货地址id,没有可不传或者传null "customer_name": "", // 客户姓名 "customer_phone": "", // 客户电话 "customer_address": "", // 客户地址 "customer_count": 1, // 客户人数或者餐具数 "total_price": 28, // 商品总价 "total_box_fee": 0, // 总餐盒费 "total_tax_fee": 0, // 总税费,调用税费计算接口获得 "delivery_fee": 0, // 配送费 "discount_fee": 0, // 用正数表示,计算 total_fee 时要减去,调用折扣计算接口获得 "service_fee": 0, // 服务费 "total_fee": 34, // 总金额 "total_fee_cny": -1, // 总金额(人民币计价),只有 在线支付 时需要换算并传入,否则传-1值或者该字段不传。当 takeout.cny_exrate>0 时 total_fee_cny=total_fee * cny_exrate,否则 total_fee_cny=total_fee。在线支付目前只支持人民币结算。 "remark": "", // 整单备注信息 "plan_time": "2019-04-28T15:22+0800", // 计划时间,ISO8601格式,对于配送,是预计(期望)送达时间;对于来取,是预计到店时间;若没有则不传。 "ignore_extra_price": true, // 是否忽略菜品中的加价菜、调味品的价格 "products": [{ // 订单商品列表 "product": 1, // 菜品id "item_id": 3, // 菜品item_id "price": 3, // 菜品价格 "unit": "C", // 菜品规格 "pu": 0, // 菜品规格索引 "req": "", // 备注,需求 "qty": 1, // 菜品数量 "box_fee": 0, // 餐盒费=单个菜品餐盒费*数量。(目前前端传的是单份菜的餐盒费,后端也按单份验证) "total_price": 3, // 商品总价=单个菜品价格*数量 "condiments": [{ // 调味品,如果没有可不传或传空列表[] "product": 33, // 调味品id "price": 0, // 调味品价格 "item_name": " no sugar" // 调味品名称 }, { "product": 34, "price": 3, "item_name": "cream" } ] }, { "product": 30, "item_id": 6004, "price": 25, "unit": "D", "pu": 0, "qty": 1, // 套餐数量(份数) "total_price": 25, "box_fee": 0, "children": [{ // 套餐中选择的菜品,套餐需要有 children, "product": 21, "item_id": 53, "price": 0, "unit": "X", "pu": 0, "qty": 1, // 单份套餐中包含的数量 "total_price": 0, "condiments": [] }, { "product": 17, "item_id": 1017, "price": 1, "unit": "C", "pu": 1, "qty": 1, "total_price": 1, "condiments": [{ // 套餐中的菜品的调味品,如果没有可不传或传空列表[] "product": 34, "price": 3, "item_name": "cream" } ] } ] } ], "discounts": [], // 折扣明细,参考 6.9 接口 "services": [] // 服务费明细,参考 6.9 接口 } ``` 提交数据有问题的话,返回状态码400。 下单成功返回状态码200,返回订单详情(数据结构参考列表接口)。 --- 1. `product` / `product.children` 数据结构基本相同。 总价计算方式: 1. 菜品/套餐的 `price` 使用单价,加价菜品、调味品 `price` 单独表示,计算 `total_price` / `total_fee` / 单个菜品/套餐总价 时再加上。 2. 菜品/套餐的 `price` 使用总价(包含加价菜品、调味品的 `price`),下单时增加 `ignore_extra_price` 进行声明。[目前采用这种方式] ### 6.6 获取支付URL * 在线支付(微信) `GET /api/v1/frontend/orders/<id>/pay-url?res_id=<res_id>&openid=<openid>&method=wechat` * 会员卡支付 `GET /api/v1/frontend/orders/<id>/pay-url?res_id=<res_id>&method=member` 在线支付目前对接两种: * 钱方 (`qf`) * 收钱吧 (`sqb`) 当餐厅使用钱方支付方式(`takeout.pay_mch == 'qf'`)时,需要获取用户 `openid`,并传入。 以下几种情况会返回 HTTP 状态码 403: * 使用钱方支付方式 且 `openid` 为空 * 订单下单时传的 **支付类型** 不是微信支付 * 订单已支付、订单已取消(关闭)、订单已完成 没有异常的情况下,返回数据结构: ``` { "url": "http://xxxxx" } ``` 若 `url` 为空字符串,表示获取支付URL失败(或异常),需要重新获取。 若 `url` 不为空,则跳转到这个 `url`,会进入 钱方/收钱吧 提供的支付页面,这个页面会唤醒微信支付。 支付成功的话,会跳转到前端的一个支付结果页面(携带 `rid` 和 `oid` (订单id)两个查询参数,例如`?rid=1&oid=3`),支付结果页需要根据下面的接口查询最终的**支付状态**。 ### 6.7 查询支付结果 `GET /api/v1/frontend/orders/<id>/is-paid?res_id=<res_id>` 返回数据结构: ``` { "is_paid": true // 是否支付 } ``` ### 6.8 计算折扣金额(优惠金额) `POST /api/v1/frontend/orders/calc-discount-fee?res_id=<res_id>` 提交数据结构: ``` { "restaurant": 1, // res_id "products": [{ "product": 549, // 菜品id "price": 4.2, // 菜品价格 "qty": 1, // 数量 "item_id": 6005, // 菜品item_id "item_name": "Garden Salad", // 菜品名称 "total_price": 4.2, // 菜品总价 } ] } ``` 返回数据结构: ``` { "discount_fee": 1.5 // 用正数表示,0表示没有折扣 } ``` ### 6.9 统一计算费用 `POST /api/v1/frontend/orders/calc-fee?res_id=<res_id>` 提交数据结构: ``` { "restaurant": 1, // res_id,必传 "table_id": 1, // 桌台ID,必传 "eat_type": 0, // 用餐类型,参考下单接口,一定要传 "distance": 0, // 配送距离,非配送订单可不传,或者传0值 "total_box_fee": 0, // 总餐盒费 "products": [{ "product": 549, // 菜品id "price": 4.2, // 菜品价格 "qty": 1, // 数量 "item_id": 6005, // 菜品item_id "item_name": "Garden Salad", // 菜品名称 "total_price": 4.2, // 菜品总价 } ] } ``` 返回数据结构: ``` { "delivery_fee": 0, // 配送费,负数表示不在配送范围,仅限配送订单使用 "discount_fee": 1.5, // 用正数表示,0表示没有折扣。包括开台折扣、会员折扣。 "service_fee": 0.6, // 额外的服务费 "tax_fee": "1.06", // 税费 "is_tax_included": false, // 是否已含税,若已含税,则 tax_fee 不计入 total_fee "discounts": [ // 折扣明细,下单时原样传回 { "name": "XXX 1 元", "value": 1 }, { "name": "5% off", "value": 0.68 }, { "name": "Discount 0.8", "is_member": true, "value": 2.38 } ], "services": [ // 服务费明细,下单时原样传回 { "name": "5% Service", "value": 0.6 } ] } ``` ## 7. 图片上传 ### 7.1 Base64 形式 `POST /api/v1/frontend/image-upload-base64` 提交参数: ``` { "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAAK0lEQVRIiWNUUNP7z0AHwEQPS0YtGrVo1KJRi0YtGrVo1KJRi0YtGj4WAQCQSgGnEEr+twAAAABJRU5ErkJggg==" // base64 图片数据 } ``` 上传成功返回图片URL: ``` { "url": "http://localhost:8040/media/orgs/gicater/wx_onvmr1aXGcOaACz-_JFfM_Eoophc.jpg" } ``` ## 8. 钱方微信支付 目前通过钱方支付对接微信支付。(暂不使用官方微信支付) 微信支付相关接口需要在微信浏览器内使用。 **特别说明**:接口前缀 `/api/v1/frontend/weixin` 改为 `/api/v1/frontend/qfpay`。 ### 8.1 [公开] 获取微信授权登录 URL `GET /api/v1/frontend/qfpay/authorize-url?res_id=<res_id>&table_id=<table_id>` * `res_id` <- `rid` * `table_id` <- `tid` 返回数据结构: ``` { "url": "https://openapi-test.qfpay.com/tool/v1/get_weixin_oauth_code?app_code=2DAB13A0AF4D4031820149BCD58188D0&redirect_uri=https%3A%2F%2Fapi.digwtx.com%3Frid%3D1&mchid=2w1exh5NlY&sign=4A6CBC119E5F948F6525EAF1E202156A" } ``` 得到 `url` 之后在微信浏览器中打开, 然后会跳转到 `redirect_uri` 对应的页面, 并且随带 `code` 查询参数, `code` 参数用于获取用户的 `openid`。 `redirect_uri` 对应的前端页面需要读取 `code` 参数,并通过下面的接口获取用户的 `openid`。 ### 8.2 [公开] 获取微信 openid `POST /api/v1/frontend/qfpay/get-weixin-openid?res_id=<res_id>` 提交参数: ``` { "code": "iweirwieriweriweirwei" // 上一步获取的 code } ``` 若 `code` 无效或已使用,则返回 HTTP 状态码 400。 返回数据结构: ``` { "openid": "olaIk1gMClTA5_RnkC7hvmhVpChE" } ``` `openid` 在微信支付时需要使用,`openid` 有可能会变化(钱方可能会有多个公众号),前端需要保存 `openid`。 ## 9. 微信 ### 9.1 获取 JSSDK 配置信息 ### 9.2 获取微信授权登录 URL `GET /api/v1/frontend/weixin/authorize-url?res_id=<res_id>&table_id=<table_id>` * `res_id` <- `rid` * `table_id` <- `tid` 返回状态码 200, 返回数据结构如下: ``` { "url": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxe1224df14973c3c4&redirect_uri=http%3A%2F%2F192.168.199.149%3A8099%2F%3Frid%3D1%26tid%3D-1&response_type=code&scope=snsapi_userinfo&state=biz:1haIYC:UXDWpHpJ6kyV4YFgbBvOsw_EFyk#wechat_redirect" } ``` 得到 `url` 之后在微信浏览器中打开. 若用户授权登录, 则跳转到 `redirect_uri`, 并且随带 `code` / `state`参数, `code` 参数用于获取 `access_token`. `redirect_uri` 对应的页面需要读取 `code` / `state` 参数。 ### 9.3 获取微信 access_token `POST /api/v1/frontend/weixin/access-token` 提交参数: ``` { "res_id": 1, // 店铺标识 "invite_code": "邀请码", // 用户的邀请码 "code": "081by3xF1rgqj803NFuF17abxF1by3xD", // 跳转到 redirect_uri 附带的 code "state": "biz%3A1h10MA%3AKSlANGsFNq96Qd2GvVTn7LDsE1A" // 跳转到 redirect_uri 附带的 state } ``` 说明: * 在未登录状态(或登录已失效)调用时,请求中不要带 `Authorization` 请求头。 * 若请求后返回401状态码,需要重试。 * 若在系统中没有跟微信用户对应用户,则会创建一个新的用户(根据微信用户微信)。 * 在有效登录状态下调用此接口,会将微信用户与当前登录用户进行绑定。 若成功获取 `access_token`, 则返回状态码 200, 返回数据结构如下(与 `3.2 登录` 接口相同): ``` { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjM0LCJpYXQiOjE1NTE3NTUwMTQsImV4cCI6MTU1MTg0MTQxNH0.NiT1uN_OKTYlTxn9TvZBTMyQKR8UBYg4g2tciKVyMIo", // 用户 token "openid": "wieriweirweir", // 在微信公众号下的openid "id": 34, // 用户ID "ttl": 86400 // token 有效期(单位秒) } ``` 否则返回状态码 400. 主要是提示 `code` / `state` 无效. ## 10. Facebook ### 10.1 验证 access_token `POST /api/v1/frontend/facebook/debug-token` 提交参数: ``` { "res_id": 1, // 餐厅ID "user_id": 23423424234, // facebook登录后得到的用户id "access_token": "081by3xF1rgqj803NFuF17abxF1by3xD", // facebook 登录后得到的 accessToken "name": "xxxx" // facebook 用户昵称 } ``` 若验证成功,则返回状态码 200, 返回数据结构如下(与 `3.2 登录` 接口相同): ``` { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjM0LCJpYXQiOjE1NTE3NTUwMTQsImV4cCI6MTU1MTg0MTQxNH0.NiT1uN_OKTYlTxn9TvZBTMyQKR8UBYg4g2tciKVyMIo", // 用户 token "openid": "wieriweirweir", // 在微信公众号下的openid "id": 34, // 用户ID "ttl": 86400 // token 有效期(单位秒) } ``` 验证失败,返回状态码400。 ## 11. 会员卡 说明: * 若用户没有开通(或绑定)会员卡,则调用这些接口会返回状态码403. ### 11.1 开卡/领卡 `POST /api/v1/frontend/user-member-card/open` 若开卡成功,返回状态码200. 异常: * 若已有会员卡,则返回状态码403. ### 11.2 绑定已有卡片 `POST /api/v1/frontend/user-member-card/bind` 提交数据: ``` { "card_id": "xxx", // 会员卡号 "card_pwd": "", // 会员卡密码,允许为空密码 } ``` 若卡号、密码验证成功且绑定成功,则返回状态码200. 异常: * 若已有会员卡,则返回状态码403. * 若会员卡已被绑定,则返回状态码400. * 若卡号或密码错误,则返回状态码400. ### 11.3 查询会员信息 `GET /api/v1/frontend/user-member-card/query` 返回数据结构: ``` { "cur_score": 480, // 当前积分 "status": 1, "card_level": "", // 卡等级名称 "discount": 0.8, // 当前折扣,0.8表示8折 "discount_desc": "", // 当前折扣描述 "balance": "361.79", // 可用余额 "card_id": "wx001" // 卡号 } ``` ### 11.4 查询消费明细(余额明细) `GET /api/v1/frontend/user-member-card/consume-logs` 返回数据结构: ``` { "count": 20, "next": "/api/v1/frontend/user-member-card/consume-logs?page=2&page_size=20", // 下一页URL,没有则为 null "previous": null, // 上一页URL,没有则为 null "results": [ // 结果集 { "ori_money": 13.6, "act_money": 13.6, // 实际金额,若打折或赠送,则与 ori_money 值会不同 "description": "消费13.6,获得积分13\n", // 说明,只有中文,不支持多语言 "org_name": "DIGWTX01", // 餐厅名称 "amount": 361.79, // 余额,最多显示2位小数 "time": "2019-08-20T11:52:40+08:00", // 时间 "type": 2 // 1-充值,2-消费 }, // ... ] } ``` ### 11.5 查询积分明细 `GET /api/v1/frontend/user-member-card/score-logs` 返回数据结构: ``` { "count": 20, "next": "/api/v1/frontend/user-member-card/score-logs?page=2&page_size=20", // 下一页URL,没有则为 null "previous": null, // 上一页URL,没有则为 null "results": [ // 结果集 { "org_name": "DIGWTX01", // 餐厅名称 "type": 7, // 见下面描述 "description": "消费13.6,获得积分13\n", // 说明,只有中文,不支持多语言 "save_score": 13, // 积分变化,正数表示增加,负数表示扣减 "time": "2019-08-20T11:52:40+08:00" // 时间 }, // ... ] } ``` 积分类型(`type`)说明: * `6`/`9` - 充值赠送 * `7` - 消费获得 * `71` - 抵扣积分 * 其他数值 - 其他 ### 11.6 解绑会员卡 `POST /api/v1/frontend/user-member-card/unbind` 若解绑成功,则返回状态码200. 前端需要刷新数据,因为会员卡没有了。 异常: * 若解绑时用户无会员卡,则返回状态码403. ## 12. 会员充值 ### 12.1 获取充值金额列表 `GET /api/v1/frontend/deposit-orders/prices` 返回数据结构: ``` { "can_order": true, // 是否可以进行充值操作 "wechat": [ // 微信支付充值金额 { "money_price": "1.00", // 计价货币金额,即实际充到会员卡的金额 "price": "5.00" // 对应的人民币金额,即实际需要支付的金额 }, { "money_price": "10.00", "price": "70.00" }, { "money_price": "100.00", "price": "700.00" } ], "alipay": [] // 支付宝支付充值金额 } ``` 目前只支持微信支付。 ### 12.2 订单列表 `GET /api/v1/frontend/deposit-orders` 返回数据结构: ``` { "count": 42, "code": 0, "next": "/api/v1/frontend/deposit-orders?page=2", "previous": null, "results": [ { "id": 50, "pay_type": "wechat", // 支付方式 "money_price": "1.00", // 要充值的金额 "price": "5.00", // 要支付的金额 "status": "success", // 状态,new-待支付,expired-已过期,success-已支付/已完成 "pay_time": "2019-08-13T11:49:48.590637+08:00", // 支付时间,未支付时为 null "expiry_time": "2019-08-13T12:04:08.912040+08:00", // 过期时间 "create_time": "2019-08-13T11:49:08.912040+08:00" // 下单时间 }, // ... ] } ``` ### 12.3 下单 `POST /api/v1/frontend/deposit-orders` 提交参数: ``` { "pay_type": "wechat", // 支付方式 "money_price": "10", // 要充值的金额 "price": "70" // 要支付的金额 } ``` `money_price` / `price` 由 `12.1` 接口提供。 下单成功返回状态码201,返回数据结构参考 列表 接口。 ### 12.4 获取支付链接 `GET /api/v1/frontend/deposit-orders/<id>/pay-url` 返回数据结构: ``` { "url": "http://xxx/xxxx" // 支付URL,跳转过去 } ``` 异常: * 若订单已过期或已支付,则返回状态码403. 支付成功后,会跳转到前端的结果页,同时在 URL 上附带餐厅ID和订单ID参数(示例 `?rid=1&doid=108` )查询参数。 ### 12.5 查询支付结果 `GET /api/v1/frontend/deposit-orders/<id>/is-paid` 返回数据结构: ``` { "is_paid": true, // 是否支付,true-已支付,false-未支付 "order": { "id": 101, "pay_type": "wechat", "money_price": "1.00", "price": "5.00", "status": "success", "pay_time": "2019-08-20T09:16:08.344958+08:00", "deposit_time": null, "expiry_time": "2019-08-20T09:30:52.853616+08:00", "create_time": "2019-08-20T09:15:52.853616+08:00" } } ``` ## 13 异常充值订单 ### 13.1 异常订单列表 `GET /api/v1/frontend/deposit-orders/exceptions` 返回数据结构: ``` { "count": 64, "code": 0, "next": "/api/v1/frontend/deposit-orders/exceptions?page=2", // 下一页URL,没有则为null "previous": null, // 上一页URL,没有则为null "results": [ // 结果集 { "id": 251, // 订单id "order_id": "D251", // 订单编号 "feedback": { // 申诉状态,未申诉则为null "status": "new", // 申诉状态,new-待处理,success-已处理 "has_images": false }, "pay_type": "wechat", // 支付类型 "money_price": "1.00", // 充值金额 "price": "5.00", // 付款金额(¥) "status": "expired", // 状态,new-待支付,expired-已过期,success-已支付/已完成 "pay_time": null, / 支付时间 "deposit_time": null, "expiry_time": "2019-09-02T18:35:52.392855+08:00", "create_time": "2019-09-02T18:20:52.392855+08:00" // 下单时间 }, // ... ] } ``` 已处理/未处理 根据 `feedback.status` 判断: * `new` 或者 `feedback`为空 - 未处理 * `success` - 已处理 已到账/未到账根据 **充值订单** 的 `status` 判断: * `new` / `expired` - 未到账 * `success` - 已到账 ### 13.2 充值订单申诉 `POST /api/v1/frontend/deposit-order-feedback` 提交数据: ``` { "order": 251, // 充值订单id "phone": "15389781522", // 联系电话 "reason": "xxx", // 申诉原因 "image_1": "", // 凭证图片1 URL "image_2": "", // 凭证图片2 URL "image_3": "", // 凭证图片3 URL } ``` 图片上传参考 `7.1` 接口。图片至少要1张。 **申诉原因** 前端固定传。 提交成功返回状态码 200. 返回状态码 400 的情况: * 订单已申诉的 * 订单已完成的 ## 14 异常收款订单 ### 14.1 异常订单列表 `GET /api/v1/frontend/orders/exceptions` 返回数据结构(主要结构参考 `6.1` 接口): ``` { "count": 1, "code": 0, "next": null, // 下一页URL,没有则为null "previous": null, "results": [ // 订单列表,数据结构参考 6.1 接口 { "id": 1073, "products": [ { "product_item_id": 6009, "product_item_name": "Spaghetti Americana", "desc": "", "pu": 0, "unit": "", "price": 7.5, "origin_price": 0.0, "qty": 1, "box_fee": 0.0, "total_price": 7.5, "thumbnail": "//pos-cn-node.oss-cn-beijing.aliyuncs.com//{90063812-9CAF-11E9-99B5-FCAA144EED3C}/6009-1.png?x-oss-process=image%2Fauto-orient%2C1%2Fresize%2Cm_fill%2Cw_256%2Ch_256%2Fquality%2Cq_85" }, { "product_item_id": 6005, "product_item_name": "Garden Salad", "desc": "", "pu": 0, "unit": "", "price": 4.2, "origin_price": 0.0, "qty": 1, "box_fee": 0.0, "total_price": 4.2, "thumbnail": "//pos-cn-node.oss-cn-beijing.aliyuncs.com//{90063812-9CAF-11E9-99B5-FCAA144EED3C}/6005-1.png?x-oss-process=image%2Fauto-orient%2C1%2Fresize%2Cm_fill%2Cw_256%2Ch_256%2Fquality%2Cq_85" } ], "table": { "table_id": -1, "table_name": "Quick Service" }, "feedback": { // 申诉状态,未申诉则为null "status": "new", // 申诉状态,new-待处理,success-已处理 "has_images": false }, "customer_name": "234", "customer_phone": "234234234234", "customer_address": "中山市兴中道28号中山市兴中体育场|||123456", "customer_count": 1, "eat_type": 1, "paid_type": 2, "is_paid": false, "paid_time": null, "paid_mch": "caijinbao", "plan_time": "2019-09-19T17:13:47.666000+08:00", "total_price": 11.7, "total_box_fee": 0.0, "delivery_fee": 1.0, "discount_fee": 2.34, "is_tax_included": false, "total_tax_fee": 0.07, "total_fee": 10.43, "total_fee_cny": 1.27, "total_fee_cny_real": -1.0, "take_code": "", "take_code_datauri": "", "lang": "zh-hans", "remark": "", "trade_no": "11568882596073", "out_trade_no": "11568882596073X97833", "out_order_id": "", "order_head_id": 0, "status": 0, "create_time": "2019-09-19T16:43:16.698245+08:00", "restaurant": 1, "user_address": 18 } ] } ``` 已处理/未处理 根据 `feedback.status` 判断: * `new` 或者 `feedback`为空 - 未处理 * `success` - 已处理 已收款/未收款根据 **is_paid** 判断: * `true` - 已收款 * `false` - 未收款 ### 14.2 订单申诉 `POST /api/v1/frontend/order-feedback` 提交数据: ``` { "order": 251, // 订单id "phone": "15389781522", // 联系电话 "reason": "xxx", // 申诉原因 "image_1": "", // 凭证图片1 URL "image_2": "", // 凭证图片2 URL "image_3": "", // 凭证图片3 URL } ``` 图片上传参考 `7.1` 接口。图片至少要1张。 **申诉原因** 前端固定传。 提交成功返回状态码 200. 返回状态码 400 的情况: * 订单已申诉的 * 订单已完成的