针对中台订单中心需要将线上线下门店,订单,商品,以及线上金额对账等进行打通,对饿了么外卖,美团外卖进行了订单对接,当前以落单,重要的订单状态流转,以及对外卖平台直接调用为实现,记录以下实现方案,后期对接外卖平台以该文档持续补充。
饿了么商家开放平台
新建应用,目前新建的是个人应用;如果需要发布正式环境,需要进行上线审核,待审核过后,才可以进行正式环境的使用。
进入沙箱环境,会有对应的测试店铺路径和账号密码,然后配置对应的推送URL
该推送URL为饿了么的推送订单以及订单状态变更的URL,需要一个Get请求的路径和Post请求的路径,Get请求路径是饿了么会校验该路径是否合法,必须要使用外网链接,如果没有,可以通过花生壳进行内网穿透
key和Secret是该应用生成token时所需要的固定参数,通过此token可以获取到该应用下所有的门店信息,其绑定是在饿了么商家中心进行绑定(测试门店未发布正式环境无法绑定)
①:URL:配置正式门店与应用关系
饿了么商家版
通过门店下店铺管理页面,对门店进行绑定(注意:门店必须已经正式上线才可以绑定,测试门店无法绑定)
新增商品时的SKU编码在饿了么落单时会通过订单明细里面的商品对应的扩展字段来下发,可以通过它来定义第三方系统的Sku编码,使饿了么的Sku编码和第三方系统的Sku编码产生映射
通过对饿了么线上网店信息的修改,也可以将线上网店里面的扩展编码存入第三方系统的线下门店编码,饿了么线上网店的扩展编码在落单时会随着订单落下,通过它可以使饿了么的网店编码和第三方系统的线下门店编码产生映射。
(饿了么测试推送URL时使用)
/**
* API层:饿了么对接POST接口
*/
@ApiOperation(value = "饿了么对接GET接口", tags = {"tradeCenterApi"}, nickname = "doGet")
@ApiResponses(value = {@ApiResponse(code = 200, message = "000000:成功,否则失败")})
String elementGet();
/**
* Controller层
*/
@Override
@GetMapping(value = "/element", produces = {"application/json"})
public String elementGet(){
log.info("测试是否接收到饿了么消息");
Map map = new HashMap();
map.put("message", "OK");
return JSONObject.toJSONString(map);
}
(饿了么真正使用的落单URL,主要通过type,消息类型字段判断此次订单落单的场景)
/**
* API层:饿了么对接POST接口
*/
@ApiOperation(value = "饿了么对接POST接口", tags = {"tradeCenterApi"}, nickname = "doPost")
@ApiResponses(value = {@ApiResponse(code = 200, message = "000000:成功,否则失败")})
String elementPost(ElemReqDto elemReqDto);
/**
* Controller层
*/
@Override
@PostMapping(value = "/element", produces = {"application/json"})
public String elementPost(@ApiParam(value = "饿了么订单信息", required = true)
@Valid @RequestBody ElemReqDto elemReqDto){
log.info("测试是否接收到饿了么消息:{}",JSONObject.toJSONString(elemReqDto));
return tradeCenterService.elementPost(elemReqDto);
}
/**
* Service层:饿了么接单以及状态流转
*/
String elementPost(ElemReqDto elemReqDto);
/**
* Service实现层:饿了么接单以及状态流转
*/
@lombok.extern.slf4j.Slf4j
@Service("tradeCenterServiceImpl")
@Transactional(rollbackFor = Exception.class)
public class TradeCenterServiceImpl implements TradeCenterService {
@Override
public String elementPost(ElemReqDto elemReqDto) {
Optional.ofNullable(elemReqDto).orElseThrow(() -> new AppException("饿了么单据不存在"));
String message = elemReqDto.getMessage();
log.info("Type类型:{};message消息体:{}", elemReqDto.getType(), message);
try {
String type = String.valueOf(elemReqDto.getType());
if (type.equals("14") || type.equals("17") || type.equals("15")) {
// TODO: 2021/6/22 取消场景:
//14==1:接单前用户取消;2:商户拒绝接单;3:5分支未接单自动取消
//** 17==订单取消(门店接单后取消) **
//15==门店接单后用户取消
type = TradeConstants.ElemEvent.ELEM_CANCEL;
}
//去工厂获取其对应的类型
ElemDomainService orderEvent = ElemFactory.getOrderEvent(type);
//根据类型找到对应的实现逻辑
orderEvent.orderEvent(message, type);
Map map = new HashMap();
map.put("message", "OK");
return JSONObject.toJSONString(map);
} catch (Exception e) {
return e.getMessage();
}
}
}
①:工厂类:注入到容器中,通过容器将参数遍历并且存放到map中
@Service
public class ElemFactory {
private static Map map = new ConcurrentHashMap();
public static ElemDomainService getOrderEvent(String type) {
return map.get(type);
}
@Autowired
public void register(ElemDomainService[] instances) {
if (instances != null && instances.length > 0) {
for (ElemDomainService oth : instances) {
map.put(oth.getEvent(), oth);
}
}
}
}
②:接口类:定义该策略的接口,一般俩个,一个为获取其对应的Type类型,一个为针对该类型找到对应的实现逻辑
public interface ElemDomainService {
/**
* 获取事件类型
* @return
*/
String getEvent();
/**
* 执行对应逻辑
*/
void orderEvent(String message,String type);
}
③:接口实现类:以落单实现类为案例
@Service
@Slf4j
public class ElemCreateServiceImpl implements ElemDomainService {
@Autowired
private WaiMaiReqDtoConvertor waiMaiReqDtoConvertor;
@Autowired
TradeCenterDomainService tradeCenterDomainService;
@Override
public String getEvent() {
//10代表订单生效,正常需要采用枚举类
return "10";
}
@Override
public void orderEvent(String message, String type) {
//将消息体的内容转换为对象
ElemOrderReqDto elemOrderReqDtos = JSONObject.parseObject(message, ElemOrderReqDto.class);
//将饿了么的消息体对象与第三方系统进行字段转换
OrderReqDto orderReqDto = waiMaiReqDtoConvertor.ElemDtoToOrderDto(elemOrderReqDtos);
//进行入库操作
tradeCenterDomainService.save(orderReqDto);
}
}
①:饿了么信息
/**
* 饿了么信息
*
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
public class ElemReqDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 应用id,应用创建时系统分配的唯一id varchar
*/
@ApiModelProperty(value = "应用id,应用创建时系统分配的唯一id")
@JsonProperty(index = 10)
private Long appId;
/**
* 消息的唯一id,用于唯一标记每个消息 varchar
*/
@ApiModelProperty(value = "消息的唯一id,用于唯一标记每个消息")
@JsonProperty(index = 10)
private String requestId;
/**
* 消息类型,参加下方【消息类型】 varchar
*/
@ApiModelProperty(value = "消息类型,参加下方【消息类型】")
@JsonProperty(index = 10)
private Integer type;
/**
* JSON格式字符串 varchar
*/
@ApiModelProperty(value = "JSON格式字符串")
@JsonProperty(index = 10)
private String message;
/**
* 商户的店铺id varchar
*/
@ApiModelProperty(value = "商户的店铺id")
@JsonProperty(index = 10)
private Long shopId;
/**
* 网店对应的门店编码 varchar
*/
@ApiModelProperty(value = "网店对应的门店编码")
@JsonProperty(index = 10)
private String storeCode;
/**
* 消息发送的时间戳,每次推送时生成,单位毫秒 varchar
*/
@ApiModelProperty(value = "消息发送的时间戳,每次推送时生成,单位毫秒")
@JsonProperty(index = 10)
private Long timestamp;
/**
* 消息的唯一id,用于唯一标记每个消息 varchar
*/
@ApiModelProperty(value = "消息的唯一id,用于唯一标记每个消息")
@JsonProperty(index = 10)
private Long userId;
/**
* 消息的唯一id,用于唯一标记每个消息 varchar
*/
@ApiModelProperty(value = "消息的唯一id,用于唯一标记每个消息")
@JsonProperty(index = 10)
private String signature;
}
②:饿了么订单信息
/**
* 饿了么订单信息
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
public class ElemOrderReqDto implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 外部流水单号 varchar
*/
@ApiModelProperty(value = "外部流水单号")
@JsonProperty(index = 10)
private Long orderId;
/**
* 顾客送餐地址 varchar
*/
@ApiModelProperty(value = "顾客送餐地址")
@JsonProperty(index = 10)
private String address;
/**
* 下单时间 varchar
*/
@ApiModelProperty(value = "下单时间")
@JsonProperty(index = 10)
private LocalDateTime createdAt;
/**
* 用户实际支付配送费 varchar
*/
@ApiModelProperty(value = "用户实际支付配送费")
@JsonProperty(index = 10)
private BigDecimal deliverFee;
/**
* 订单备注 varchar
*/
@ApiModelProperty(value = "订单备注")
@JsonProperty(index = 10)
private String description;
/**
* 店铺Id varchar
*/
@ApiModelProperty(value = "店铺Id")
@JsonProperty(index = 10)
private Long shopId;
/**
* 网店对应的门店编码 varchar
*/
@ApiModelProperty(value = "网店对应的门店编码")
@JsonProperty(index = 10)
private String storeCode;
/**
* 订单状态 varchar
*/
@ApiModelProperty(value = "订单状态")
@JsonProperty(index = 10)
private String status;
/**
* 订单总价 varchar
*/
@ApiModelProperty(value = "订单总价")
@JsonProperty(index = 10)
private BigDecimal totalPrice;
/**
* 订单原价 varchar
*/
@ApiModelProperty(value = "订单原价")
@JsonProperty(index = 10)
private BigDecimal originalPrice;
/**
* 订单收货人姓名 varchar
*/
@ApiModelProperty(value = "订单收货人姓名")
@JsonProperty(index = 10)
private String consignee;
/**
* 订单业务类型 (0外卖单,1到店自取订单,2企业到店买单) varchar
*/
@ApiModelProperty(value = "订单业务类型 (0外卖单,1到店自取订单,2企业到店买单)")
@JsonProperty(index = 10)
private String orderBusinessType;
/**
* 订单明细 varchar
*/
@ApiModelProperty(value = "订单明细")
@JsonProperty(index = 10)
private List groups;
/**
* 订单参加活动信息 varchar
*/
@ApiModelProperty(value = "订单参加活动信息")
@JsonProperty(index = 10)
private List orderActivities;
/**
* 顾客联系电话 varchar
*/
@ApiModelProperty(value = "顾客联系电话")
@JsonProperty(index = 10)
private List phoneList;
}
③:饿了么订单明细信息
/**
* 饿了么订单明细信息
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
public class ElemOrderLineReqDto implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 分组名称 varchar
*/
@ApiModelProperty(value = "分组名称")
@JsonProperty(index = 10)
private String name;
/**
* 类别:normal:普通商品;discount:赠品 varchar
*/
@ApiModelProperty(value = "类别:normal:普通商品;discount:赠品")
@JsonProperty(index = 10)
private String type;
/**
* 商品明细 varchar
*/
@ApiModelProperty(value = "商品明细")
@JsonProperty(index = 10)
private List items;
}
④:饿了么订单明细商品信息
/**
* 饿了么订单明细商品信息
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
public class ElemItemReqDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 商品名称 varchar
*/
@ApiModelProperty(value = "商品名称")
@JsonProperty(index = 10)
private String name;
/**
* SkuId varchar
*/
@ApiModelProperty(value = "SkuId")
@JsonProperty(index = 10)
private Long skuId;
/**
* 商品分类Id varchar
*/
@ApiModelProperty(value = "商品分类Id")
@JsonProperty(index = 10)
private String categoryId;
/**
* 商品单价 varchar
*/
@ApiModelProperty(value = "商品单价")
@JsonProperty(index = 10)
private BigDecimal price;
/**
* 总价 varchar
*/
@ApiModelProperty(value = "总价")
@JsonProperty(index = 10)
private BigDecimal total;
/**
* 商品数量 varchar
*/
@ApiModelProperty(value = "商品数量")
@JsonProperty(index = 10)
private Integer quantity;
/**
* SkuCode
*/
@ApiModelProperty(value = "SkuCode")
@JsonProperty(index = 10)
private String extendCode;
}
⑤:订单转换类(针对自己字段进行修改)
/**
* 饿了么订单落单时转换为中心订单(正向)
*
* @param elemOrderReqDto
* @return
*/
public static OrderReqDto ElemDtoToOrderDto(ElemOrderReqDto elemOrderReqDto) {
OrderReqDto orderReqDto = OrderReqDto.builder()
.orderIdOut(String.valueOf(elemOrderReqDto.getOrderId()))
.saleTime(elemOrderReqDto.getCreatedAt())
.tradeType(TradeConstants.TradeType.ELEM)
.orderType(TradeConstants.OrderType.ELEMENT)
.storeType(TradeConstants.storeType.STORE)
//此处的openId代表我们的第三方门店编码,可以直接使用,也可以去数据库查询
.storeCode(elemOrderReqDto.getOpenId())
.phoneNumber(String.valueOf(elemOrderReqDto.getPhoneList()))
.payableAmount(elemOrderReqDto.getTotalPrice())
.actualAmount(elemOrderReqDto.getTotalPrice())
.orderAmount(elemOrderReqDto.getOriginalPrice())
.freightAmount(elemOrderReqDto.getDeliverFee())
.orderSplit(TradeConstants.orderSplit.NO_SPLIT)
.orderSupportReverse(TradeConstants.orderSupportReverse.SUPPORT_REVERSE)
.orderFlatFlag(TradeConstants.FlatFlag.NO_FLAT_FLAG_NOMAL).build();
//收货地址订单
OrderAddressReqDto addressReqDto = OrderAddressReqDto.builder()
.detailAddress(elemOrderReqDto.getAddress())
.receiver(elemOrderReqDto.getConsignee()).build();
orderReqDto.setAddress(addressReqDto);
//扩展信息
OrderExtReqDto orderExtReqDto = OrderExtReqDto.builder()
.logisticsStatus(TradeConstants.LogisticsStatus.Logistics)
.sendTime(LocalDateTime.now()).build();
orderReqDto.setOrderExt(orderExtReqDto);
//订货信息
OrderReservationReqDto orderReservationResDto = OrderReservationReqDto.builder()
.takeSendTime(LocalDateTime.now())
.reservationUserName(elemOrderReqDto.getConsignee())
.phoneNumber(String.valueOf(elemOrderReqDto.getPhoneList()))
.productionStoreCode(elemOrderReqDto.getOpenId())
.takeSendStoreCode(elemOrderReqDto.getOpenId())
.build();
orderReqDto.setReservationResDto(orderReservationResDto);
//订单明细信息
List elemOrderLineReqDtoList = elemOrderReqDto.getGroups().stream()
.filter(v -> v.getType().equals("normal"))
.collect(Collectors.toList());
List orderLineReqDtoList = new ArrayList<>();
elemOrderLineReqDtoList.get(0).getItems().forEach(v -> {
OrderLineReqDto orderLineReqDto = OrderLineReqDto.builder()
.actualAmount(v.getTotal())
.originPrice(v.getPrice())
.actualPrice(v.getPrice())
//ExtendCode为我们第三方的商品编码,也可以直接使用美团
.itemCode(v.getExtendCode())
.skuCode(v.getExtendCode())
.itemName(v.getName())
.skuQuantity(BigDecimal.valueOf(v.getQuantity()))
.storeCode(String.valueOf(elemOrderReqDto.getShopId()))
.build();
orderLineReqDtoList.add(orderLineReqDto);
});
orderReqDto.setDetailList(orderLineReqDtoList);
return orderReqDto;
}
/**
* API层:
*/
@ApiOperation(value = "确认/拒绝接单", tags = {"tradeCenterApi"})
@ApiImplicitParams({
@ApiImplicitParam(name = "orderNo", value = "订单号(逗号拼接,支持多个)", paramType = "query"),
@ApiImplicitParam(name = "Type", value = "接单:(10:有赞;20:饿了么;30:美团);拒单(-10:有赞;-20:饿了么;-30:美团)", paramType = "query")
})
@ApiResponses(value = {@ApiResponse(code = 200, message = "操作是否成功,000000:成功,否则失败", response = MultiOrderResDto.class)})
ResultDTO conFirmOrder(@ApiIgnore @RequestParam Map params);
/**
* Controller层:
*/
@Override
@GetMapping(value = "/conFirmOrder", produces = {"application/json"})
public ResultDTO conFirmOrder(@ApiIgnore @RequestParam Map params) {
tradeCenterService.conFirmOrder(params);
return ResultDTO.ok();
}
/**
* @Description
* @Author: dingjunxin
* @Email: [email protected]
*/
@Service
@Slf4j
public class ElemOrderConfirmServiceImpl implements OrderConfirmDomainService {
@Autowired
TradeCenterDomainService tradeCenterDomainService;
@Override
public String getEvent() {
return TradeConstants.OrderConfirmType.ELEMENT;
}
@Override
public void orderEvent(OrderResDto orderResDto, String type) {
try {
//此处主要使用饿了么网店编码进行确认接单
OrderService orderService = WaiMaiUtil.getOrderService(data);
//此处需要传入饿了么订单号进行确认接单
orderService.confirmOrderLite(orderResDto.getOrderIdOut());
} catch (Exception e) {
throw new AppException(e.getMessage());
}
}
}
/**
* 获取OrderService
*/
public static OrderService getOrderService() {
//todo 以上正常需要传入网店信息,去找其对应的应用秘钥,当前注释,不再描述
// 当前应用appKey
String appKey = "fqRp1D9y1v";
// 当前应用secret
String appSecret = "d5c2caf79c4708539f81ff0701160510b3982837";
// 实例化一个配置类
Config config = getConfig(false,appKey,appSecret);
Token token = getToken(config);
System.out.println(token);
OrderService orderService = new OrderService(config, token);
return orderService;
}
/**
* 获取配置类
* @param isSandbox 是否沙箱
* @return
*/
public static Config getConfig(Boolean isSandbox,String appKey,String appSecret){
Config config=null;
if(isSandbox){
config=new Config(isSandbox, appKey,appSecret);
}else{
// TODO 填充正式环境数据
config=new Config(isSandbox, appKey,appSecret);
}
return config;
}
/**
* 获取对应Token
*/
public static Token getToken(Config config){
// 使用config对象,实例化一个授权类
OAuthClient client = new OAuthClient(config);
// 使用授权类获取token
Token token = client.getTokenInClientCredentials();
return token;
}
美团开放平台-为美好智慧生活连接更多可能
目前美团可以通过品牌商和服务商的身份入住,当前以品牌商身份进行对接,美团只有一个开发环境,并且当测试门店绑定手机号需要联系美团人员进行绑定,否则找不到对应的测试门店,无法测试下单。
①:URL:配置美团的商家绑定对应门店,
当前无法确定门店网店,商品映射关系的参数
②:appId和AppSecret用于生成调用美团API的sig
③:美团网店与第三方门店绑定
描述:通过该授权页面,可以将美团对应的网店和我们的第三方门店进行绑定,但是有一个问题和饿了么不同,美团推送订单时,所携带的APP_POI_CODE,是可以直接下发我们的门店编码,如果我们没有进行配置,则默认下发美团的网店ID,而如果我们将美团的修改为第三方门店的编码,则直接下发我们的门店编码,此字段只有一个,可以通过页面进行配置,但是由于扩展性,建议落库。
(正向,逆向,状态流转)
/**
* 美团正向单
*/
@ApiOperation(value = "美团正向单", tags = {"mTPost"}, nickname = "doPost")
@ApiResponses(value = {@ApiResponse(code = 200, message = "000000:成功,否则失败")})
Object mTPost(MtReqDto request);
/**
* 美团逆向单
*/
@ApiOperation(value = "美团逆向单", tags = {"mTReversePost"}, nickname = "doPost")
@ApiResponses(value = {@ApiResponse(code = 200, message = "000000:成功,否则失败")})
Object mTReversePost(@RequestParam Map params);
/**
* 美团订单状态流转POST接口
*/
@ApiOperation(value = "美团订单状态流转POST接口", tags = {"TriggerMtOrder"}, nickname = "doPost")
@ApiResponses(value = {@ApiResponse(code = 200, message = "000000:成功,否则失败")})
Object TriggerMtOrder(@RequestParam Map params);
@Override
@PostMapping(value = "/mTPost", produces = {"text/html"})
public Object mTPost(MtReqDto mtReqDto){
log.info("测试是否接收到美团正向消息:{}",JSONObject.toJSONString(mtReqDto));
return tradeCenterService.MtTrigger(mtReqDto);
}
@Override
@GetMapping(value = "/mTReversePost", produces = {"text/html"})
public Object mTReversePost(Map params){
MtReverseReqDto mtReverseReqDto = MtofBeanUtils.mapToObject(params, MtReverseReqDto.class);
log.info("测试是否接收到美团逆向消息:{}",JSONObject.toJSONString(mtReverseReqDto));
return tradeCenterService.MtReverseTrigger(mtReverseReqDto);
}
@Override
@GetMapping(value = "/TriggerMtOrder")
public Object TriggerMtOrder(Map params){
log.info("测试是否接收到美团取消消息:{}",JSONObject.toJSONString(params));
//9:取消(针对某种场景的取消)
MtReqDto mtReqDto= MtReqDto.builder()
.order_id(Long.valueOf(params.get("order_id").toString()))
.status("9")
.build();
return tradeCenterService.MtTrigger(mtReqDto);
}
/**
* 美团正向接单以及状态流转
*/
Object MtTrigger(MtReqDto mtReqDto);
/**
* 美团逆向接单以及状态流转
*/
Object MtReverseTrigger(MtReverseReqDto mtReqDto);
@Override
public Object MtTrigger(MtReqDto reqDto) {
Optional.ofNullable(reqDto).orElseThrow(() -> new AppException("美团单据不存在"));
reqDto.setDetail(URLUtil.decode(reqDto.getDetail()));
reqDto.setPoi_receive_detail(URLUtil.decode(reqDto.getPoi_receive_detail()));
String message = URLUtil.decode(JSONObject.toJSONString(reqDto));
MtReqDto mtReqDto = JSONObject.parseObject(message, MtReqDto.class);
try {
String type = "MT" + mtReqDto.getStatus();
//策略+工厂
MtDomainService orderEvent = MtFactory.getOrderEvent(type);
orderEvent.orderEvent(mtReqDto, type);
Map map = new HashMap();
return map.put("data", "OK");
} catch (Exception e) {
return e.getMessage();
}
}
@Override
public Object MtReverseTrigger(MtReverseReqDto reqDto) {
Optional.ofNullable(reqDto).orElseThrow(() -> new AppException("美团单据不存在"));
String message = URLUtil.decode(JSONObject.toJSONString(reqDto));
MtReverseReqDto mtReverseReqDto = JSONObject.parseObject(message, MtReverseReqDto.class);
try {
String type = "MT_REVERSE_" + mtReverseReqDto.getRes_type();
//1;3;7;8:取消
if (mtReverseReqDto.getRes_type() == 1 || mtReverseReqDto.getRes_type() == 3
|| mtReverseReqDto.getRes_type() == 7 || mtReverseReqDto.getRes_type() == 8) {
type = "MT_REVERSE_CANCEL";
} else if (mtReverseReqDto.getRes_type() == 2 || mtReverseReqDto.getRes_type() == 4
|| mtReverseReqDto.getRes_type() == 5 || mtReverseReqDto.getRes_type() == 6) {
//2;4;5;6:完成
type = "MT_REVERSE_FINISH";
}else if (mtReverseReqDto.getRes_type()==0&& CollectionUtils.isNotEmpty(mtReverseReqDto.getFood())){
type="MT_REVERSE_PART";
}
MtDomainService orderEvent = MtFactory.getOrderEvent(type);
orderEvent.orderReverseEvent(mtReverseReqDto, type);
Map map = new HashMap();
return map.put("data", "OK");
} catch (Exception e) {
return e.getMessage();
}
}
①:美团订单表头
/**
* 美团订单表头
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
@Builder
public class MtReqDto implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 订单ID(数据库中请用bigint(20)存储此字段)
*/
private Long order_id;
/**
* 订单展示ID
*/
private Long wm_order_id_view;
/**
* APP方商家ID
*/
private String app_poi_code;
/**
* 美团商家名称
*/
private String wm_poi_name;
/**
* 美团商家地址
*/
private String wm_poi_address;
/**
* 美团商家电话
*/
private String wm_poi_phone;
/**
* 收件人地址(此字段为用户填写的收货地址,可在开发者中心订阅是否根据经纬度反查地址,若订阅则会在此字段后追加反查结果,并用“@#”符号分隔,如:用户填写地址@#反查结果)
*/
private String recipient_address;
/**
* 收件人电话(请兼容13812345678和13812345678_123456两种号码格式,以便对接隐私号订单,最多不超过20位)
*/
private String recipient_phone;
/**
* 备用隐私号 ["13812345678_1236","13812345678_3456"]
*/
private List backup_recipient_phone;
/**
* 收件人姓名(若用户没有填写姓名,此字段默认为空。可在开发者中心订阅是否用“美团客人”填充此字段)
*/
private String recipient_name;
/**
* 门店配送费
*/
private BigDecimal shipping_fee;
/**
* 总价
*/
private BigDecimal total;
/**
* 原价
*/
private BigDecimal original_price;
/**
* 忌口或备注
*/
private String caution;
/**
* 送餐员电话
*/
private String shipper_phone;
/**
* 订单状态 2:新订单-用户支付完成,待商家接单 4:商家已接单 8:订单已完成 9:订单已取消
*/
private String status;
/**
* 城市ID(目前暂时用不到此信息)
*/
private Long city_id;
/**
* 是否开发票
*/
private Integer has_invoiced;
/**
* 发票抬头
*/
private String invoice_title;
/**
* 纳税人识别号,该信息默认不推送,如有需求可在开发者中心订阅
*/
private String taxpayer_id;
/**
* 创建时间 (注:订单创建时间)
*/
private Long ctime;
/**
* 更新时间
*/
private Long utime;
/**
* 用户预计送达时间,“立即送达”时为0,非0 代表非即时单(包含到店自取,时间为用户到店取餐时间),预订单代表用户下单时填写的预计送达时间,单位是秒,10位时间戳
*/
private Long delivery_time;
/**
* 是否是第三方配送平台配送,0表否,1表是)
*/
private Integer is_third_shipping;
/**
* 支付类型,1表货到付款,2表在线支付(非支付渠道)
*/
private Integer pay_type;
/**
* 取餐类型(0:普通取餐;1:到店取餐),该信息默认不推送,如有需求可在开发者中心订阅
*/
private Integer pick_type;
/**
* 实际送餐地址纬度
*/
private Double latitude;
/**
* 实际送餐地址经度
*/
private Double longitude;
/**
* 门店当天的推单流水号,该信息默认不推送,如有需求可在开发者中心订阅
*/
private Integer day_seq;
/**
* 用户是否收藏此门店(true, false),该信息默认不推送,如有需求可在开发者中心订阅
*/
private Boolean is_favorites;
/**
* 用户是否第一次在此门店点餐(true, false),该信息默认不推送,如有需求可在开发者中心订阅
*/
private Boolean is_poi_first_order;
/**
* 用餐人数(0:用户没有选择用餐人数;1-10:用户选择的用餐人数;-10:10人以上用餐;88:用户需要餐具;99:用户不需要餐具),该信息默认不推送,如有需求可在开发者中心订阅
*/
private String dinners_number;
/**
* 订单配送方式,该信息默认不推送,如有需求可在开发者中心订阅
*/
private String logistics_code;
/**
* 商家对账信息的json数据,该信息默认不推送,如有需求可在开发者中心订阅
*/
private String poi_receive_detail;
/**
* 第一种:优惠信息,其值为由list
②:美团订单明细信息
/**
* 美团订单明细信息
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
public class MtOrderLineReqDto implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 分组名称 varchar
*/
@ApiModelProperty(value = "分组名称")
@JsonProperty(index = 10)
private String app_food_code;
/**
* sku编码 varchar
*/
@ApiModelProperty(value = "sku编码")
@JsonProperty(index = 10)
private String sku_id;
/**
* 商品数量 varchar
*/
@ApiModelProperty(value = "商品数量")
@JsonProperty(index = 10)
private BigDecimal quantity;
/**
* 商品单价,不包含餐盒费,此字段默认为活动折扣后价格,可在开发者中心订阅是否替换为原价 varchar
*/
@ApiModelProperty(value = "商品单价,不包含餐盒费,此字段默认为活动折扣后价格,可在开发者中心订阅是否替换为原价")
@JsonProperty(index = 10)
private BigDecimal price;
/**
* 餐盒数量,在计算餐盒数量和餐盒费用时,请先按照商品规格维度将餐盒数量向上取整后,再乘以相应的餐盒费单价,计算得出餐盒费用。 varchar
*/
@ApiModelProperty(value = "餐盒数量,在计算餐盒数量和餐盒费用时,请先按照商品规格维度将餐盒数量向上取整后,再乘以相应的餐盒费单价,计算得出餐盒费用。")
@JsonProperty(index = 10)
private String box_num;
/**
* 餐盒价格 varchar
*/
@ApiModelProperty(value = "餐盒价格")
@JsonProperty(index = 10)
private BigDecimal box_price;
/**
* 单位 varchar
*/
@ApiModelProperty(value = "单位")
@JsonProperty(index = 10)
private String unit;
/**
* 商品折扣,默认为1,仅美团商家可设置 varchar
*/
@ApiModelProperty(value = "商品折扣,默认为1,仅美团商家可设置")
@JsonProperty(index = 10)
private String food_discount;
/**
* 菜品名称 varchar
*/
@ApiModelProperty(value = "菜品名称")
@JsonProperty(index = 10)
private String food_name;
}
③:美团逆向订单表头
/**
* 美团逆向订单表头
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
@Builder
public class MtReverseReqDto implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 逆向ID
*/
private String refund_id;
/**
* 订单ID(数据库中请用bigint(20)存储此字段)
*/
private Long order_id;
/**
* 通知类型:apply:发起退款;agree:确认退款;reject:驳回退款;cancelRefund:用户取消退款申请
*/
private String notify_type;
/**
* 退款原因
*/
private String reason;
/**
* 0:未处理;1:商家驳回退款请求;2、商家同意退款;3、客服驳回退款请求;4、客服帮商家同意退款;5、超过3小时自动同意;6、系统自动确认;7:用户取消退款申请;8:用户取消退款申诉
*/
private Integer res_type;
/**
* 是否申诉退款:0-否;1-是
*/
private Integer is_appeal;
/**
* 退款金额
* 消息类型msg_type:
* 1:全部退款申请
* 2:全部退款申请处理(同意or拒绝)
* 3:全部退款退款成
* 4:全部退款失败
* 11:部分退款申请
* 12:部分退款申请处理(同意or拒绝)
* 13:部分退款退款成功
* 14:部分款退款失败
* 23:重复支付退款)
* 当 msg_type > 10时,有金额money退款字段
*/
private String money;
/**
* 退款商品信息
*/
private List food;
}
④:美团逆向订单明细
/**
* 美团逆向订单明细
*/
@Validated
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel
@Builder
public class ItemLineReqDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* APP方菜品id,最大长度128,不同门店可以重复,同一门店内不能重复 varchar
*/
@ApiModelProperty(value = "APP方菜品id,最大长度128,不同门店可以重复,同一门店内不能重复")
@JsonProperty(index = 10)
private String app_food_code;
/**
* 退款菜品名称
*/
@ApiModelProperty(value = "退款菜品名称")
@JsonProperty(index = 10)
private String food_name;
/**
* sku码
*/
@ApiModelProperty(value = "sku码")
@JsonProperty(index = 10)
private String sku_id;
/**
* 单位
*/
@ApiModelProperty(value = "单位")
@JsonProperty(index = 10)
private String spec;
/**
* 商品价格
*/
@ApiModelProperty(value = "商品价格")
@JsonProperty(index = 10)
private BigDecimal food_price;
/**
* 商品数量
*/
@ApiModelProperty(value = "商品数量")
@JsonProperty(index = 10)
private Integer count;
/**
* 打包盒数量
*/
@ApiModelProperty(value = "打包盒数量")
@JsonProperty(index = 10)
private BigDecimal box_num;
/**
* 打包盒价格
*/
@ApiModelProperty(value = "打包盒价格")
@JsonProperty(index = 10)
private BigDecimal box_price;
/**
* 菜品原价,单位元
*/
@ApiModelProperty(value = "菜品原价,单位元")
@JsonProperty(index = 10)
private BigDecimal origin_food_price;
/**
* 退款价格,单位元
*/
@ApiModelProperty(value = "退款价格,单位元")
@JsonProperty(index = 10)
private BigDecimal refund_price;
}
⑤:转换类
/**
* 美团订单落单时转换为中心订单(正向)
*
* @param
* @return
*/
public static OrderReqDto MtDtoToOrderDto(MtReqDto mtReqDto) {
OrderReqDto orderReqDto = OrderReqDto.builder()
.orderIdOut(String.valueOf(mtReqDto.getOrder_id()))
.saleTime(LocalDateTime.now())
.tradeType(TradeConstants.TradeType.MT)
.orderType(TradeConstants.OrderType.MT)
.storeType(TradeConstants.storeType.STORE)
//该字段代表的可以是美团自己生成的ID,也可以使用我们第三方的门店编码,通过门店授权进行绑定
.storeCode(meiTuanReqDto.getApp_poi_code())
.payableAmount(mtReqDto.getTotal())
.actualAmount(mtReqDto.getTotal())
.orderAmount(mtReqDto.getOriginal_price())
.freightAmount(mtReqDto.getShipping_fee())
.orderSplit(TradeConstants.orderSplit.NO_SPLIT)
.orderSupportReverse(TradeConstants.orderSupportReverse.SUPPORT_REVERSE)
.orderFlatFlag(TradeConstants.FlatFlag.NO_FLAT_FLAG_NOMAL).build();
//地址信息
OrderAddressReqDto addressReqDto = OrderAddressReqDto.builder()
.detailAddress(mtReqDto.getRecipient_address())
.receiver(mtReqDto.getRecipient_name()).build();
orderReqDto.setAddress(addressReqDto);
//扩展信息
OrderExtReqDto orderExtReqDto = OrderExtReqDto.builder()
.logisticsStatus(TradeConstants.LogisticsStatus.Logistics).build();
orderReqDto.setOrderExt(orderExtReqDto);
//订货信息
OrderReservationReqDto orderReservationResDto = OrderReservationReqDto.builder()
.takeSendTime(LocalDateTime.now())
.reservationUserName(mtReqDto.getRecipient_name())
.phoneNumber(mtReqDto.getRecipient_phone())
.productionStoreCode(meiTuanReqDto.getApp_poi_code())
.takeSendStoreCode(meiTuanReqDto.getApp_poi_code()).build();
orderReqDto.setReservationResDto(orderReservationResDto);
//明细信息
List mtOrderLineReqDtoList = JSONArray.parseArray(mtReqDto.getDetail(), MtOrderLineReqDto.class);
List orderLineReqDtoList = new ArrayList<>();
mtOrderLineReqDtoList.forEach(v -> {
OrderLineReqDto orderLineReqDto = OrderLineReqDto.builder()
.actualAmount(v.getPrice())
.originPrice(v.getPrice())
.actualPrice(v.getPrice())
//该字段为我们自定义的商品编码,也可使用美团自己生成的,但字段名非这个
.itemCode(v.getSku_id())
.skuCode(v.getSku_id())
.itemName(v.getFood_name())
.skuQuantity(v.getQuantity())
.storeCode(meiTuanReqDto.getApp_poi_code())
.build();
orderLineReqDtoList.add(orderLineReqDto);
});
orderReqDto.setDetailList(orderLineReqDtoList);
return orderReqDto;
}
/**
* 美团订单落单时转换为中心订单(逆向全额)
*/
public static ReverseOrderReqDto MtReverseToOrderDto(MtReverseReqDto mtReverseReqDto, OrderResDto orderResDto) {
BigDecimal money = BigDecimal.ZERO;
if (mtReverseReqDto.getMoney() != null) {
money = new BigDecimal(mtReverseReqDto.getMoney());
}
ReverseOrderReqDto reverseOrderDto = ReverseOrderReqDto.builder()
.orderIdOut(mtReverseReqDto.getRefund_id())
.orderNo(orderResDto.getOrderNo())
.tradeType(TradeConstants.TradeType.MT_REVERSE)
.saleTime(LocalDateTime.now())
.storeType(orderResDto.getStoreType())
.storeCode(orderResDto.getStoreCode())
.relateOrderType(TradeConstants.relateOrderType.GENERAL_RELATE_ORDER)
.actualAmount(mtReverseReqDto.getMoney() != null ? money : null)
.parentOrderNo(orderResDto.getOrderNo())
.relateOriginNo(orderResDto.getOrderNo())
.reverseType(TradeConstants.relateOrderType.GENERAL_RELATE_ORDER)
.remark(mtReverseReqDto.getReason()).build();
List orderLineReqDtoList = new ArrayList<>();
orderResDto.getDetailList().forEach(v -> {
ReverseOrderLineReqDto reverseOrderLineReqDto = ReverseOrderLineReqDto.builder()
.orderNo(orderResDto.getOrderNo())
.storeCode(orderResDto.getStoreCode())
.skuCode(v.getSkuCode())
.itemCode(v.getItemCode())
.itemName(v.getItemName())
.payableAmount(v.getActualAmount())
.actualAmount(v.getActualAmount())
.skuQuantity(v.getSkuQuantity())
.refundType(TradeConstants.relateOrderType.GENERAL_RELATE_ORDER).build();
orderLineReqDtoList.add(reverseOrderLineReqDto);
});
reverseOrderDto.setDetailList(orderLineReqDtoList);
return reverseOrderDto;
}
前面如上,采用策略+工厂模式进行扩展,不再描述
@Service
@Slf4j
public class MtOrderConfirmServiceImpl implements OrderConfirmDomainService {
@Autowired
private WaiMaiReqDtoConvertor waiMaiReqDtoConvertor;
@Autowired
TradeCenterDomainService tradeCenterDomainService;
@Autowired
private RestTemplateUtils restTemplateUtils;
@Override
public String getEvent() {
return TradeConstants.OrderConfirmType.MT;
}
@Override
public void orderEvent(OrderResDto orderResDto, String type) {
try {
//获取Sig
long timestamp = WaiMaiUtil.getMtTimestamp();
String md5Input = DAConfigConst.Url.MT_CONFIRM + "?app_id=" + WaiMaiUtil.MtConfig.appId
+ "&order_id=" + orderResDto.getOrderIdOut()
+ "×tamp=" + timestamp + WaiMaiUtil.MtConfig.appSecret;
//此Sig为调用美团Api接口必传参数,由美团URL加API上列举系统参数进行MD5加密生成
//可通过API控制台先行测试
String sig = WaiMaiUtil.getSig(md5Input);
//调用美团参数
Map params = new MapUtils()
.put("app_id", WaiMaiUtil.MtConfig.appId)
.put("order_id", orderResDto.getOrderIdOut())
.put("timestamp", timestamp)
.put("sig", sig);
log.info("确认接单参数:{}", JSONObject.toJSONString(params));
//此处采用工具类直接调用外部接口,DA为URL路径
String result = HttpUtil.sendGet(DAConfigConst.Url.MT_CONFIRM, params);
//此处针对返回报错信息抛出对应异常
JSONObject jsonObject = JSONObject.parseObject(result);
if (jsonObject.get("error")!=null){
MtErrorLineVo error = JSONObject.parseObject(jsonObject.get("error").toString(), MtErrorLineVo.class);
throw new AppException(error.getMsg());
}
} catch (Exception e) {
throw new AppException(e.getMessage());
}
}
}
public static class MtConfig{
public static final String appId="4746";
public static final String appSecret="ec3498174474570a57bcb540dbd537cb";
}
/**
* 获取时间戳
* @return
*/
public static long getMtTimestamp(){
LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
return ldt.toEpochSecond(ZoneOffset.ofHours(8));
}
/**
* MD5加密Sig
* @param sig
* @return
*/
public static String getSig(String sig){
return Md5Util.encryptMessage(sig);
}
/**
* @Description Md5加密工具类
*/
public class Md5Util {
/**
* 加密明文
*
* 加密步骤:
* 1.先进行MD5加密
* 4.加密后的明文转换为16进制
*
* @param message 明文
* @return
*/
public static String encryptMessage(String message) {
/**
* 进行Md5加密
*/
byte[] d5Sha1Data = DigestUtils.md5(EncodingUtils.getBytes(
(message == null ? "" : message),
"utf-8"));
/**
* 加密后的信息转换为16进制
*/
StringBuilder builder = new StringBuilder();
for (byte b : d5Sha1Data) {
builder.append(String.format("%02x", new Integer(b & 0xff)));
}
return builder.toString();
}