# H5免登接口
# 1.概述
客户平台通过H5免登方式使用美团企业版服务,根据productType等字段传参,跳转对应页面。
注意点:
1.前端不可使用iframe方式接入
2.免登接口返回的重定向链接不可多次使用,每次登录都须调用免登接口
3.免登成功后,将会写入美团相关的 Cookie 信息。请不要手动操作/清理/覆盖 Web 容器中美团域名下的 Cookie,否则将会导致登录状态失效,频繁登录,帐号被风控等异常问题的出现
# 2.接口基本信息
| 名称 | 描述 |
|---|---|
| 请求方式 | POST |
| 调用地址 | 测试环境:https://waimai-openapi.apigw.test.meituan.com/api/sqt/open/login/h5/loginFree/redirection?test_open_swimlane=test-open 正式环境: https://bep-openapi.meituan.com/api/sqt/open/login/h5/loginFree/redirection |
| 调用方 | 客户平台 |
| 响应方 | 美团企业版 |
| 响应超时时间 | 3秒 |
| 调用限频 | 每分钟访问不超过100次,每天累计访问不超过100000次 |
# 2.1.请求体
| 名称 | 类型 | 是否必填 | 示例 | 说明 |
|---|---|---|---|---|
| accessKey | String | 是 | 美团企业版分配给客户的接入秘钥 | |
| content | String | 是 | UgxoCGPQIzoP | 请求体内容,将请求参数JSON序列化后进行加密的结果值,参照:签名实例 |
# content加密前数据结构
| 名称 | 类型 | 必填 | 示例 | 说明 |
|---|---|---|---|---|
| ts | Long | 是 | 1617085650321 | 13位时间戳。若请求发起时间与平台接受请求时间相差大于10分钟,平台将直接拒绝本次请求 |
| entId | Long | 是 | 2789 | 固定值,由美团企业版提供 |
| staffInfo | Object | 是 | { "staffPhone": "18842600000", "staffEmail": "zhangsan@company.com", "staffNum": "123123DFSAS23", "staffName": "张三", "orgName": "部门1", "externalOrgId": "organization1", "invoiceTitle": "xxxx公司", "invoiceNum": "税号" } | staffPhone 员工手机号 staffEmail 员工邮箱 staffNum 员工工号 staffName 员工姓名 orgName: 部门名称 externalOrgId: 部门唯一标识 invoiceTitle: 发票抬头 invoiceNum: 税号 1. 登录时, staffPhone/staffEmail/staffNum 三者必填一个, 与企业员工唯一识别对应 2. 部门信息, 发票, 税号非必填 3. 自动添加员工时, 对应信息将填充进员工信息中 |
| ssoUser | String | 否 | {"id":"1","name":"a"} | 自定义键值对,key、value均可自定义,对接第三方收银台透传用户额外信息时使用,会在调用第三方收银台-下单接口时,通过extInfoMap字段透传给企业 |
| nonce | String | 是 | 3c72e9102112e346c19d9460d1ff15b0a5d2 | 每次请求时,随机生成36位由字母数字组成的字符串 |
| productType | String | 是 | dp_canyin: 到店商家列表页 mt_waimai: 外卖商家列表页 sqt_center: 工作台 sqt_info: 我的 sqt_home: 美团企业版首页 mt_hotel: 美团企业版酒店预定页 mt_car: 美团打车 mt_maicai: 美团买菜 sqt_peican: 配餐首页 sqt_order_detail: 订单详情 repay_order_detail: 偿还单详情 order_list: 订单列表 audit_detail: 审批详情 mt_maoyan: 猫眼电影 mt_maiyao: 美团买药 pao_tui: 美团跑腿 mt_wmvoucher: 商品券 以下只支持ToB企业 mt_train: 火车票 mt_plane: 飞机票 | 产品类型 |
| sceneType | Integer | 否 | 1 | 场景类型 1: 商务宴请 2: 企业用车 3: 商务差旅 4: 工作餐 5: 团建用餐 8: 员工福利 9: 供给分销 |
| bizParam | Object | 否 | 如productType=dp_canyin时, 传递 { "channelType": 1, "lockCityType: " 1, ...... } | 根据productType,按照下表进行参数传递 |
# bizParam参数结构
| 业务类型 | 名称 | 类型 | 必填 | 示例 | 说明 |
|---|---|---|---|---|---|
| 餐饮 dp_canyin | location | Location | 否 | 见Location | 经纬度信息 |
| budgetKey | String | 否 | 12345 | 第三方预算key,接入第三方预算时必传 | |
| poiDetailJson | String | 否 | {"mtPoiId":"165548321","ruleId":-1} | 门店详情,跳转门店详情时必传,mtPoiId和ruleId参数不可变 | |
| channelType | Integer | 否 | 1 | 当productType=dp_canyin时,跳转页面路由,不传值时默认跳转餐饮列表 0: 餐饮列表-企业允许的全量到店商家列表 1: 团购列表-可以下团购单的商家列表,且仅展示团购支付方式 2: 特许餐饮列表 | |
| lockCityType | Integer | 否 | 1 | 是否限制只能查询当前城市中的餐厅信息 0: 不限制 1: 限制-以Location中地址为准,不允许切换 | |
| restaurantType | Integer | 否 | 1 | 选择餐厅类型-当在申请环节预选餐厅时会需要用到此参数 1: 跳转筛选页面 2: 跳转详情页面-当需要跳转详情页面时,restaurantId必传 | |
| restaurantId | Long | 否 | 12345 | restaurantType=2时必填 选择餐厅业务id,美团企业版推送的id值(点确认选择提交时) | |
| repastApplyExtraJson | RepastApplyExtraJson | 否 | 见RepastApplyExtraJson | 用餐申请单信息 | |
| 外卖 mt_waimai | location | Location | 否 | 见Location | 经纬度信息 |
| wmExtraJson | WmExtraJson | 否 | 见WmExtraJson | 外卖收货地址拓展参数,当需要通过免登传外卖收货地址时,此参数必传 | |
| budgetKey | String | 否 | 12345 | 第三方预算key,接入第三方预算时必传 | |
| restaurantType | Integer | 否 | 1 | 选择餐厅类型-当在申请环节预选餐厅时会需要用到此参数 1: 跳转筛选页面 2: 跳转详情页面-当需要跳转详情页面时,restaurantId必传 | |
| restaurantId | Long | 否 | 12345 | restaurantType=2时必填 选择餐厅业务id,美团企业版推送的id值(点确认选择提交时) | |
| thirdOrderId | String | 否 | 1234dd5 | 三方订单号,用于外卖订单详情页跳转,功能已停用 新客户订单详情跳转直接使用productType=sqt_order_detail | |
| repastApplyExtraJson | RepastApplyExtraJson | 否 | 见RepastApplyExtraJson | 用餐申请单信息 | |
| 酒店 mt_hotel | budgetKey | String | 否 | 12345 | 第三方预算key,接入第三方预算时必传 |
| tmcApplyExtraJson | TmcApplyExtraJson | 否 | 见TmcApplyExtraJson | 出差申请单信息 | |
| 火车票 mt_train | budgetKey | String | 否 | 12345 | 第三方预算key,接入第三方预算时必传 |
| tmcApplyExtraJson | TmcApplyExtraJson | 否 | 见TmcApplyExtraJson | 出差申请单信息 | |
| 飞机票 mt_plane | budgetKey | String | 否 | 12345 | 第三方预算key,接入第三方预算时必传 |
| tmcApplyExtraJson | TmcApplyExtraJson | 否 | 见TmcApplyExtraJson | 出差申请单信息 | |
| 订单详情 sqt_order_detail | sqtBizOrderId | String | 否 | - | 美团企业版订单ID,跳转订单详情(productType=sqt_order_detail)时必传 |
| 偿还单详情 repay_order_detail | sqtBizOrderId | String | 否 | - | 美团企业版订单ID,跳转偿还单详情(productType=repay_order_detail)时必传 |
| 审批详情 audit_detail | processInstanceId | String | 是 | 123456 | 审批实例id,productType为audit_detail时必传 |
| 猫眼电影 mt_maoyan | location | Location | 否 | 见Location | 经纬度信息 |
| 美团买药 mt_maiyao | location | Location | 否 | 见Location | 经纬度信息 |
| budgetKey | String | 否 | 12345 | 第三方预算key,接入第三方预算时必传 | |
| 美团跑腿 pao_tui | location | Location | 否 | 见Location | 经纬度信息 |
| budgetKey | String | 否 | 12345 | 第三方预算key,接入第三方预算时必传 |
# 参数类型说明
| 类型 | 字段 | 类型 | 示例 | 说明 |
|---|---|---|---|---|
| Location | longitude | String | 116.480881 | 经度 |
| latitude | String | 39.989410 | 纬度 | |
| geotype | String | gcj02(火星坐标系)或者wgs84(国际坐标系) | 坐标系(与经纬度一起用来定位) | |
| address | String | 北京市朝阳区阜通东大街6号 | 经纬度对应的中文地址 | |
| WmExtraJson | addressUserName | String | 张三 | 姓名 |
| addressPhone | String | 13112345678 | 手机号码 | |
| addressGender | Integer | 1 | 性别,字段枚举值为: 0-未知;1-先生;2-女士 | |
| addressHouseNumber | String | 融新3F01室 | 门牌号 | |
| policyKey | String | 1 | policyKey值为多入口模式下的供给规则对应参数,与供给规则中填写的值一致即可匹配 | |
| RepastApplyExtraJson | applyNo | String | 5NQZ0NBUDZPD | 美团用餐申请单号(原repastApplyNo) |
| externalApplyNo | String | 5N9D0AD96RY92 | 企业用餐申请单号(原outRepastApplyNo) | |
| TmcApplyExtraJson | applyNo | Long | 33006 | 美团申请单ID(原tripId) |
| externalApplyNo | String | xxx-test-4 | 企业申请单ID(原outTripId) | |
| tripId | Long | 16758 | 美团行程ID(原itineraryId) | |
| externalTripId | String | externalTripId1 | 企业行程ID(原outItineraryId) |
# 2.2.响应参数
Http 302 跳转
如参数正确,则跳转至目标产品页。如果当前用户首次进入,会进入手机号绑定界面
如参数有误或异常情况,则跳转至错误页
# 3.示例
# 3.1.请求示例
# 3.1.1.请求示例
{
"accessKey":"B3KSWLDSKSKDMJ",
"content":"UgxoCGPQIzoP"
}
# 3.1.2.请求参数content解析
{
"ts": 100746,
"entId": 100837,
"staffInfo": {
"staffPhone": "15603309067",
"invoiceTitle": "测试发票",
"invoiceNum": "12222222"
},
"nonce": "5e8097cd-2344-4ee7-be67-81f541403329",
"productType": "dp_canyin",
"sceneType": 1,
"bizParam": {
"location": {
"longitude": "116.480881",
"latitude": "39.989410",
"geotype": "gcj02",
"address": "北京市朝阳区阜通东大街6号"
},
"tmcApplyExtraJson": {
"applyNo": 33006,
"externalApplyNo": "xxx-test-4",
"tripId": 16758,
"externalTripId": "externalTripId1"
},
"repastApplyExtraJson": {
"applyNo": "5NQZ0NBUDZPD",
"externalApplyNo": "5N9D0AD96RY92"
}
}
}
# 3.2.响应示例
# 3.2.1.响应结果
HTTP/1.1 302 Found
M-TraceId: xxxxxxxxxxxxxxxxxxx
Location: <跳转链接>
# 4.错误码
| 错误代码 | 描述 | 解决方案 |
|---|---|---|
| 10100000 | 参数校验失败 | 请根据页面报错,确认字段传值是否符合规范标准,如staffPhone字段不符合大陆手机号格式等 |
| 10100001 | 必填参数缺失 | 请根据页面报错,补充必填字段传值,如productType字段不可为空 |
| 10100002 | 短时间内收到了重复请求 | 请检查是否重复发送了免登请求 |
| 10100003 | entId 不合法 | 请检查entId是否正确,且和调用环境相匹配,例如使用测试环境的entId调用了线上环境免登地址 |
| 10100004 | 员工不存在 | 请到【美团企业版企业管理后台-员工管理-组织架构】菜单中查询员工是否已同步成功 |
| 10100005 | 部门名称或唯一标识错误 | 检查orgName和externalOrgId字段传值,当需要同步部门信息时,两个字段必须同时有值 |
| 10100006 | 保存部门失败 | 重新调用接口,如若仍不成功,请联系客户经理进行排查 |
| 10100007 | 发票抬头或税号错误 | 检查invoiceTitle和invoiceNum字段传值,当需要同步发票信息时,两个字段必须同时有值 |
| 10100008 | 保存发票失败 | 重新调用接口,如若仍不成功,请联系客户经理进行排查 |
| 40100000 | 获取跳转链接失败 | 重新调用接口,如若仍不成功,请联系客户经理进行排查 |
# 5.代码实例
依赖SDK包地址:SDK下载地址
# 5.1 Java代码实例
import com.google.common.collect.Maps;
import com.meituan.sqt.constant.CommonConstants;
import com.meituan.sqt.model.HttpResponse;
import com.meituan.sqt.request.in.login.LoginRequest.LoginStaffInfo;
import com.meituan.sqt.request.in.login.LoginRequest;
import com.meituan.sqt.response.in.login.LoginApiInfo;
import com.meituan.sqt.utils.EncryptUtil;
import com.meituan.sqt.utils.HttpClientUtil;
import com.meituan.sqt.utils.JsonUtil;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.Map;
/**
* @author niliwei
* @description 登录接口使用实例
* @since 2023/4/3 7:35 PM
*/
public class LoginDemo {
/**
* 测试环境免登接口url
* test_open_swimlane为测试环境特有值,无需关注
*/
private static String LOGIN_URL_TEST = "https://waimai-openapi.apigw.test.meituan.com/api/sqt/open/login/h5/loginFree/redirection?test_open_swimlane=test-open";
/**
* 正式环境免登接口url
*/
private static String LOGIN_URL_PROD = "https://bep-openapi.meituan.com/api/sqt/open/login/h5/loginFree/redirection";
public static void main(String[] args) throws Exception {
// 免登接口正常需要客户端/前端发起调用 基于此有两种登录
//第一种登录方式(推荐,需客户端/前端+服务端介入):服务端提供给客户端/前端免登接口url及请求体,客户端/前端以post方式调用免登接口,免登接口会通过302(重定向)方式跳转到对应的页面
clientInvokeLogin();
// 第二种登录方式(需服务端介入):服务端以post方式调用免登接口,获取接口返回的302(重定向)链接,然后服务端可重定向至该链接完成登录
// !!!重要:接口返回的302链接不可重复使用,必须每次登录时调用接口重新生成
serverInvokeLogin();
}
private static void clientInvokeLogin() throws Exception {
LoginApiInfo loginApiInfo = new LoginApiInfo();
// 判断环境决定用那个参数,这里以测试环境示例
loginApiInfo.setUrl(LOGIN_URL_TEST);
loginApiInfo.setAccessKey(CommonConstants.accessKey);
loginApiInfo.setContent(buildLoginRequestBody());
System.out.println(JsonUtil.object2Json(loginApiInfo));
}
private static void serverInvokeLogin() throws Exception {
Map<String, String> headerMap = Maps.newHashMap();
headerMap.put("Accept", "application/json");
headerMap.put("Content-type", "application/x-www-form-urlencoded; charset=utf-8");
Map<String, String> requestParamMap = Maps.newHashMap();
requestParamMap.put("accessKey", CommonConstants.accessKey);
requestParamMap.put("content", buildLoginRequestBody());
HttpResponse httpResponse = null;
try {
httpResponse = HttpClientUtil.invokePost(LOGIN_URL_TEST, headerMap, requestParamMap, 2000, 3000);
// 如需json方式调用接口,参考以下注释代码
// httpResponse = HttpClientUtil.invokePost2Json(LOGIN_URL_TEST, JsonUtil.object2Json(requestParamMap), 2000,3000);
// 成功
if (httpResponse.getStatus() == 302) {
System.out.println("url = " + httpResponse.getHeader().get("Location").get(0));
return;
}
// 失败场景,根据返回值具体参考:https://h5.dianping.com/app/bep-docs/open-platform-doc/login/login_free.html#_4-%E9%94%99%E8%AF%AF%E7%A0%81
System.out.println("httpResponse = " + JsonUtil.object2Json(httpResponse));
} catch (Exception e) {
// 调用接口超时或其他场景 可以根据需要增加重试
System.out.println("e = " + e);
}
}
private static String buildLoginRequestBody() throws Exception {
LoginRequest loginRequest = new LoginRequest();
loginRequest.setTs(System.currentTimeMillis());
loginRequest.setEntId(CommonConstants.entId);
loginRequest.setStaffInfo(buildLoginStaffInfo());
loginRequest.setNonce(RandomStringUtils.random(36, true, true));
loginRequest.setProductType("mt_waimai");
// 这里如果要使用bizParam,并且map的value不是String和包装类型(Integer等),需自行定义java类
// loginRequest.setBizParam(null);
String rawContent = JsonUtil.object2Json(loginRequest, false);
String content = EncryptUtil.aesEncrypt(rawContent, CommonConstants.secretKey);
return content;
}
private static LoginStaffInfo buildLoginStaffInfo() {
LoginStaffInfo loginStaffInfo = new LoginStaffInfo();
// 传企业的唯一标识,这里以手机号为例
loginStaffInfo.setStaffPhone("19987888888");
return loginStaffInfo;
}
}
# 5.2 C#代码实例
try
{
var task = Task.Run(async () => await MeiTuanPost.MTWaimaiLogin(para.UserID)).Result;
retunr task.ToString();
}
catch (Exception ex)
{
return null;
}
MeiTuanPost类中的方法:
public static async Task<string> MTWaimaiLogin(int p_intPersonID)
{
string strTime = GetTimeStamp();
string strJson = "{ " +
"\"ts\": " + strTime + "," +
"\"entId\": " + m_tokenMeituan.EntId + "," +
"\"staffInfo\": {" +
" \"staffNum\": \"" + p_intPersonID.ToString().Trim() + "\"" + //人员唯一标识
"}," +
" \"nonce\": \"5e8097cd-2344-4ee7-be67-81f541403329\"," +
" \"productType\": \"mt_waimai\"" +
"}";
string strReurn = await PostToMTAsync(m_tokenMeituan, strJson);
return strReurn;
}
public static string GetTimeStamp()
{
DateTime dteStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1, 0, 0, 0));
DateTime dteNow = DateTime.Now;
long t = (dteNow.Ticks - dteStart.Ticks) / 10000;
return t.ToString().Trim();
}
public static async Task<string> PostToMTAsync(TokenMeituan p_tokenMeituan, string p_strContent)
{
string jsonResponse;
try {
HttpClientHandler handler = new HttpClientHandler();
handler.AllowAutoRedirect = false;
HttpClient httpClient = new HttpClient(handler);
string strcontent = EncryptUtil.AESEncode(p_strContent, p_tokenMeituan.SecretKey);
List<KeyValuePair<string, string>> dict = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("accessKey", p_tokenMeituan.AccessKey),//ApiConfigManager.MeiTuanTakeoutAccessKey
new KeyValuePair<string, string>("content", strcontent)//loginRequestBody
};
FormUrlEncodedContent formUrlEncodedContent = new FormUrlEncodedContent(dict);
formUrlEncodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
formUrlEncodedContent.Headers.ContentType.CharSet = "utf-8";
var response = await httpClient.PostAsync(p_tokenMeituan.Url, formUrlEncodedContent);
if (response.StatusCode == HttpStatusCode.Redirect)
{
return response.Headers.Location.ToString();
} else {
/// 当返回不是302的时候,可以抛出一个异常或者返回一个错误码,进行处理错误情况
}
jsonResponse = await response.Content.ReadAsStringAsync();
} catch (Exception e) {
/// 处理异常信息
}
return jsonResponse;
}
public class EncryptUtil
{
#region AES加密
/// <summary>
/// AES加密
/// </summary>
/// <param name="text">明文</param>
/// <param name="key">密钥,长度为16的字符串</param>
/// <returns>密文</returns>
public static string AESEncode(string text, string key)
{
byte[] keys = Convert.FromBase64String(key);
RijndaelManaged rijndaelCipher = new RijndaelManaged();
rijndaelCipher.Mode = CipherMode.ECB;
rijndaelCipher.Padding = PaddingMode.PKCS7;
rijndaelCipher.KeySize = 128;
rijndaelCipher.BlockSize = 128;
byte[] pwdBytes = keys;
byte[] keyBytes = new byte[16];
int len = pwdBytes.Length;
if (len > keyBytes.Length)
len = keyBytes.Length;
Array.Copy(pwdBytes, keyBytes, len);
rijndaelCipher.Key = keyBytes;
ICryptoTransform transform = rijndaelCipher.CreateEncryptor();
byte[] plainText = Encoding.UTF8.GetBytes(text);
byte[] cipherBytes = transform.TransformFinalBlock(plainText, 0, plainText.Length);
return ConvertHelper.ToBase64StringURLSafe(cipherBytes);//输出为Base64
}
#endregion
#region AES解密
/// <summary>
/// AES解密
/// </summary>
/// <param name="text">密文</param>
/// <param name="key">密钥,长度为16的字符串</param>
/// <returns>明文</returns>
public static string AESDecode(string text, string key)
{
RijndaelManaged rijndaelCipher = new RijndaelManaged();
rijndaelCipher.Mode = CipherMode.ECB;
rijndaelCipher.Padding = PaddingMode.PKCS7;
rijndaelCipher.KeySize = 128;
rijndaelCipher.BlockSize = 128;
byte[] encryptedData = ConvertHelper.FromBase64StringURLSafe(text);
byte[] pwdBytes = Convert.FromBase64String(key);
byte[] keyBytes = new byte[16];
int len = pwdBytes.Length;
if (len > keyBytes.Length)
len = keyBytes.Length;
Array.Copy(pwdBytes, keyBytes, len);
rijndaelCipher.Key = keyBytes;
ICryptoTransform transform = rijndaelCipher.CreateDecryptor();
byte[] plainText = transform.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
return Encoding.UTF8.GetString(plainText);
}
#endregion
}
# 6.版本记录
| 版本号 | 版本日期 | 更新内容 |
|---|---|---|
| v1.0 | 2022-11-21 | 新增H5免登接口 |