文章目录
本文是对仿12306项目进行的代码分析,主要集中在购票操作这一个最难的模块
1. controller 层
首先前端的请求会进入ConfirmOrderController类中的doConfirm方法。
1.1 ConfirmOrderController类
doConfirm方法
@SentinelResource(value = "confirmOrderDo", blockHandler = "doConfirmBlock")
@PostMapping("/do")
public CommonResp<Object> doConfirm(@Valid @RequestBody ConfirmOrderDoReq req) {
if (!env.equals("dev")) {
// 图形验证码校验
.....
}
Long id = beforeConfirmOrderService.beforeDoConfirm(req);
return new CommonResp<>(String.valueOf(id));
}
我们来看一下传入doConfirm方法的请求参数req的结构
会员id private Long memberId;
日期 private Date date;
车次编号 private String trainCode;
出发站 private String start;
到达站 private String end;
余票ID private Long dailyTrainTicketId;
车票 private List<ConfirmOrderTicketReq> tickets;
验证码 private String imageCode;
图片验证码token private String imageCodeToken;
日志跟踪号 private String logId;
加入排队人数,用于体验排队功能 private int lineNumber;
2. service层
2.1 BeforeConfirmOrderService类
beforeDoConfirm方法
public Long beforeDoConfirm(ConfirmOrderDoReq req)
Long id = null;
Date date = req.getDate();
String trainCode = req.getTrainCode();
String start = req.getStart();
String end = req.getEnd();
List<ConfirmOrderTicketReq> tickets = req.getTickets();
保存确认订单表,状态初始
DateTime now = DateTime.now();
ConfirmOrder confirmOrder = new ConfirmOrder();
confirmOrder.setId(SnowUtil.getSnowflakeNextId());
confirmOrder.setCreateTime(now);
confirmOrder.setUpdateTime(now);
confirmOrder.setMemberId(req.getMemberId());
confirmOrder.setDate(date);
confirmOrder.setTrainCode(trainCode);
confirmOrder.setStart(start);
confirmOrder.setEnd(end);
confirmOrder.setDailyTrainTicketId(req.getDailyTrainTicketId());
confirmOrder.setStatus(ConfirmOrderStatusEnum.INIT.getCode());
confirmOrder.setTickets(JSON.toJSONString(tickets));
confirmOrderMapper.insert(confirmOrder);
看一下ConfirmOrder 这个类的参数
private Long id;
private Long memberId;
private Date date;
private String trainCode;
private String start;
private String end;
private Long dailyTrainTicketId;
private String status;
private Date createTime;
private Date updateTime;
private String tickets;
接下来,发送MQ排队购票
ConfirmOrderMQDto confirmOrderMQDto = new ConfirmOrderMQDto();
confirmOrderMQDto.setDate(req.getDate());
confirmOrderMQDto.setTrainCode(req.getTrainCode());
confirmOrderMQDto.setLogId(MDC.get("LOG_ID"));
String reqJson = JSON.toJSONString(confirmOrderMQDto);
LOG.info("排队购票,发送mq开始,消息:{}", reqJson);
rocketMQTemplate.convertAndSend(RocketMQTopicEnum.CONFIRM_ORDER.getCode(), reqJson);
LOG.info("排队购票,发送mq结束");
id = confirmOrder.getId();
return id;
看一下ConfirmOrderMQDto这个类的参数
日志流程号,用于同转异时,用同一个流水号 private String logId;
日期 private Date date;
车次编号 private String trainCode;
2.2 ConfirmOrderConsumer 类
onMessage 方法
接收消息队列的消息,调用doConfirm方法进行具体的购票逻辑
@Override
public void onMessage(MessageExt messageExt) {
byte[] body = messageExt.getBody();
ConfirmOrderMQDto dto = JSON.parseObject(new String(body), ConfirmOrderMQDto.class);
MDC.put("LOG_ID", dto.getLogId());
LOG.info("ROCKETMQ收到消息:{}", new String(body));
confirmOrderService.doConfirm(dto);
}
2.3 ConfirmOrderService 类
doConfirm 方法
@Async
@SentinelResource(value = "doConfirm", blockHandler = "doConfirmBlock")
public void doConfirm(ConfirmOrderMQDto dto)
将日志 ID 放入日志上下文中,便于跟踪日志。
代码使用 Redis 的 setIfAbsent(相当于 Redis 的 SETNX 命令)尝试获取分布式锁,防止多个线程同时处理同一个订单。
构建分布式锁的键值,锁定的维度是日期和列车代码。
尝试获取锁,如果获取成功,则进入锁定状态,否则返回。
MDC.put("LOG_ID", dto.getLogId());
LOG.info("异步出票开始:{}", dto);
String lockKey = RedisKeyPreEnum.CONFIRM_ORDER + "-" + DateUtil.formatDate(dto.getDate()) + "-" + dto.getTrainCode();
// setIfAbsent就是对应redis的setnx
Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(setIfAbsent)) {
LOG.info("恭喜,抢到锁了!lockKey:{}", lockKey);
} else {
// 只是没抢到锁,并不知道票抢完了没,所以提示稍候再试
// LOG.info("很遗憾,没抢到锁!lockKey:{}", lockKey);
// throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_LOCK_FAIL);
LOG.info("没抢到锁,有其它消费线程正在出票,不做任何处理");
return;
}
创建一个 ConfirmOrderExample 对象,用于构建查询条件
如果查询到的订单列表为空,则输出日志并退出循环。
如果不为空,则输出日志并继续处理查询到的订单。
使用 forEach 遍历订单列表,对每个订单调用 sell(confirmOrder) 方法进行售票处理
在 finally 块中记录日志表示购票流程结束,并删除 Redis 中的锁(lockKey),释放锁资源
try {
while (true) {
// 取确认订单表的记录,同日期车次,状态是I,分页处理,每次取N条
ConfirmOrderExample confirmOrderExample = new ConfirmOrderExample();
confirmOrderExample.setOrderByClause("id asc");
ConfirmOrderExample.Criteria criteria = confirmOrderExample.createCriteria();
criteria.andDateEqualTo(dto.getDate())
.andTrainCodeEqualTo(dto.getTrainCode())
.andStatusEqualTo(ConfirmOrderStatusEnum.INIT.getCode());
PageHelper.startPage(1, 5);
List<ConfirmOrder> list = confirmOrderMapper.selectByExampleWithBLOBs(confirmOrderExample);
if (CollUtil.isEmpty(list)) {
LOG.info("没有需要处理的订单,结束循环");
break;
} else {
LOG.info("本次处理{}条订单", list.size());
}
// 一条一条的卖
list.forEach(confirmOrder -> {
try {
sell(confirmOrder);
} catch (BusinessException e) {
if (e.getE().equals(BusinessExceptionEnum.CONFIRM_ORDER_TICKET_COUNT_ERROR)) {
LOG.info("本订单余票不足,继续售卖下一个订单");
confirmOrder.setStatus(ConfirmOrderStatusEnum.EMPTY.getCode());
updateStatus(confirmOrder);
} else {
throw e;
}
}
});
}
} finally {
// try finally不能包含加锁的那段代码,否则加锁失败会走到finally里,从而释放别的线程的锁
LOG.info("购票流程结束,释放锁!lockKey:{}", lockKey);
redisTemplate.delete(lockKey);
// LOG.info("购票流程结束,释放锁!");
// if (null != lock && lock.isHeldByCurrentThread()) {
// lock.unlock();
// }
}
}
sell 方法
首先来看一下sell方法的传入参数 ConfirmOrder,详见 beforeDoConfirm 方法
private void sell(ConfirmOrder confirmOrder)
构建 ConfirmOrderDoReq 对象,将 confirmOrder 中的相关信息复制到请求对象中
ConfirmOrderDoReq req = new ConfirmOrderDoReq();
req.setMemberId(confirmOrder.getMemberId());
req.setDate(confirmOrder.getDate());
req.setTrainCode(confirmOrder.getTrainCode());
req.setStart(confirmOrder.getStart());
req.setEnd(confirmOrder.getEnd());
req.setDailyTrainTicketId(confirmOrder.getDailyTrainTicketId());
req.setTickets(JSON.parseArray(confirmOrder.getTickets(), ConfirmOrderTicketReq.class));
req.setImageCode("");
req.setImageCodeToken("");
req.setLogId("");
将订单状态更新为 PENDING(处理中),以避免该订单被重复处理
LOG.info("将确认订单更新成处理中,避免重复处理,confirm_order.id: {}", confirmOrder.getId());
confirmOrder.setStatus(ConfirmOrderStatusEnum.PENDING.getCode());
updateStatus(confirmOrder);
根据订单的日期、车次、起始站和终点站,从数据库中查询相应的余票记录
Date date = req.getDate();
String trainCode = req.getTrainCode();
String start = req.getStart();
String end = req.getEnd();
List<ConfirmOrderTicketReq> tickets = req.getTickets();
DailyTrainTicket dailyTrainTicket = dailyTrainTicketService.selectByUnique(date, trainCode, start, end);
LOG.info("查出余票记录:{}", dailyTrainTicket);
DailyTrainTicket 的结构是
private Long id;
private Date date;
private String trainCode;
private String start;
private String startPinyin;
private Date startTime;
private Integer startIndex;
private String end;
private String endPinyin;
private Date endTime;
private Integer endIndex;
private Integer ydz;
private BigDecimal ydzPrice;
private Integer edz;
private BigDecimal edzPrice;
private Integer rw;
private BigDecimal rwPrice;
private Integer yw;
private BigDecimal ywPrice;
private Date createTime;
private Date updateTime;
预先扣减余票数量,并检查余票是否足够
reduceTickets(req, dailyTrainTicket);
创建一个空的 finalSeatList 列表,用于存储最终选中的座位信息
获取订单中第一张票的座位信息。如果第一张票中包含座位信息(即用户指定了座位),进入选座逻辑。
List<DailyTrainSeat> finalSeatList = new ArrayList<>();
// 计算相对第一个座位的偏移值
// 比如选择的是C1,D2,则偏移值是:[0,5]
// 比如选择的是A1,B1,C1,则偏移值是:[0,1,2]
ConfirmOrderTicketReq ticketReq0 = tickets.get(0);
看看ConfirmOrderTicketReq的结构
乘客ID private Long passengerId;
乘客票种 private String passengerType;
乘客名称 private String passengerName;
乘客身份证 private String passengerIdCard;
座位类型code private String seatTypeCode;
选座,可空,值示例:A1 private String seat;
根据座位类型(如经济舱、一等座等),获取对应的座位列信息
生成两排参照座位列表,作为选座的参考。比如,对于A、C、D、F列,参照列表可能是 {A, C, D, F, A, C, D, F}
if (StrUtil.isNotBlank(ticketReq0.getSeat())) {
LOG.info("本次购票有选座");
// 查出本次选座的座位类型都有哪些列,用于计算所选座位与第一个座位的偏离值
List<SeatColEnum> colEnumList = SeatColEnum.getColsByType(ticketReq0.getSeatTypeCode());
LOG.info("本次选座的座位类型包含的列:{}", colEnumList);
// 组成和前端两排选座一样的列表,用于作参照的座位列表,例:referSeatList = {A1, C1, D1, F1, A2, C2, D2, F2}
List<String> referSeatList = new ArrayList<>();
for (int i = 1; i <= 2; i++) {
for (SeatColEnum seatColEnum : colEnumList) {
referSeatList.add(seatColEnum.getCode() + i);
}
}
LOG.info("用于作参照的两排座位:{}", referSeatList);
计算每个选定座位在参照列表中的位置(即绝对偏移值)。比如,C1在列表中第2个位置,D2在第5个位置。
计算每个选定座位相对于第一个座位的偏移值。例如,如果选择了C1和D2,则相对偏移值为 [0, 5]。
List<Integer> offsetList = new ArrayList<>();
// 绝对偏移值,即:在参照座位列表中的位置
List<Integer> aboluteOffsetList = new ArrayList<>();
for (ConfirmOrderTicketReq ticketReq : tickets) {
int index = referSeatList.indexOf(ticketReq.getSeat());
aboluteOffsetList.add(index);
}
LOG.info("计算得到所有座位的绝对偏移值:{}", aboluteOffsetList);
for (Integer index : aboluteOffsetList) {
int offset = index - aboluteOffsetList.get(0);
offsetList.add(offset);
}
LOG.info("计算得到所有座位的相对第一个座位的偏移值:{}", offsetList);
根据计算出的相对偏移值,调用 getSeat 方法执行具体的选座操作,并将选中的座位信息存入 finalSeatList。
getSeat(finalSeatList,
date,
trainCode,
ticketReq0.getSeatTypeCode(),
ticketReq0.getSeat().split("")[0], // 从A1得到A
offsetList,
dailyTrainTicket.getStartIndex(),
dailyTrainTicket.getEndIndex()
);
如果用户没有指定座位,则为每张票调用 getSeat 方法进行自动选座。
} else {
LOG.info("本次购票没有选座");
for (ConfirmOrderTicketReq ticketReq : tickets) {
getSeat(finalSeatList,
date,
trainCode,
ticketReq.getSeatTypeCode(),
null,
null,
dailyTrainTicket.getStartIndex(),
dailyTrainTicket.getEndIndex()
);
}
}
LOG.info("最终选座:{}", finalSeatList);
在选座成功后,执行后续的事务处理,包括:
- 更新座位表中的售票信息。
- 更新余票详情表中的余票信息。
- 为会员增加购票记录。
- 更新确认订单状态为成功。
try {
afterConfirmOrderService.afterDoConfirm(dailyTrainTicket, finalSeatList, tickets, confirmOrder);
} catch (Exception e) {
LOG.error("保存购票信息失败", e);
throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
}
getSeat 方法
private void getSeat(List<DailyTrainSeat> finalSeatList, Date date, String trainCode, String seatType, String column, List<Integer> offsetList, Integer startIndex, Integer endIndex)
/**
* 挑座位,如果有选座,则一次性挑完,如果无选座,则一个一个挑
* @param date
* @param trainCode
* @param seatType
* @param column
* @param offsetList
*/
从数据库中查找符合指定日期、车次和座位类型的所有车厢,并保存到 carriageList 中
List<DailyTrainSeat> getSeatList = new ArrayList<>();
List<DailyTrainCarriage> carriageList = dailyTrainCarriageService.selectBySeatType(date, trainCode, seatType);
LOG.info("共查出{}个符合条件的车厢", carriageList.size());
遍历每一个符合条件的车厢,并从数据库中获取该车厢的所有座位信息,存储在 seatList 中。
for (DailyTrainCarriage dailyTrainCarriage : carriageList) {
LOG.info("开始从车厢{}选座", dailyTrainCarriage.getIndex());
getSeatList = new ArrayList<>();
List<DailyTrainSeat> seatList = dailyTrainSeatService.selectByCarriage(date, trainCode, dailyTrainCarriage.getIndex());
LOG.info("车厢{}的座位数:{}", dailyTrainCarriage.getIndex(), seatList.size());
遍历当前车厢中的所有座位,逐一进行判断,尝试选择座位
for (int i = 0; i < seatList.size(); i++) {
DailyTrainSeat dailyTrainSeat = seatList.get(i);
Integer seatIndex = dailyTrainSeat.getCarriageSeatIndex();
String col = dailyTrainSeat.getCol();
检查当前座位是否已经被选中过(防止同一批票选中同一个座位)。如果已经被选中,则跳过这个座位,继续判断下一个座位。
boolean alreadyChooseFlag = false;
for (DailyTrainSeat finalSeat : finalSeatList) {
if (finalSeat.getId().equals(dailyTrainSeat.getId())) {
alreadyChooseFlag = true;
break;
}
}
if (alreadyChooseFlag) {
LOG.info("座位{}被选中过,不能重复选中,继续判断下一个座位", seatIndex);
continue;
}
如果有指定列号(如A、B、C等),则检查当前座位的列号是否匹配。如果不匹配,则继续判断下一个座位。
if (StrUtil.isBlank(column)) {
LOG.info("无选座");
} else {
if (!column.equals(col)) {
LOG.info("座位{}列值不对,继续判断下一个座位,当前列值:{},目标列值:{}", seatIndex, col, column);
continue;
}
}
调用 calSell 方法,判断当前座位在指定区间内是否可以选。如果可以选中,则将其加入 getSeatList。
boolean isChoose = calSell(dailyTrainSeat, startIndex, endIndex);
if (isChoose) {
LOG.info("选中座位");
getSeatList.add(dailyTrainSeat);
} else {
continue;
}
如果有偏移值(即用户选了多个连续座位),则逐个验证偏移位置的座位是否可选。如果有任何一个偏移位置不可选,则放弃当前车厢中的所有已选座位,继续选择下一个车厢。
if (CollUtil.isNotEmpty(offsetList)) {
LOG.info("有偏移值:{},校验偏移的座位是否可选", offsetList);
for (int j = 1; j < offsetList.size(); j++) {
Integer offset = offsetList.get(j);
int nextIndex = i + offset;
if (nextIndex >= seatList.size()) {
LOG.info("座位{}不可选,偏移后的索引超出了这个车箱的座位数", nextIndex);
isGetAllOffsetSeat = false;
break;
}
DailyTrainSeat nextDailyTrainSeat = seatList.get(nextIndex);
boolean isChooseNext = calSell(nextDailyTrainSeat, startIndex, endIndex);
if (isChooseNext) {
LOG.info("座位{}被选中", nextDailyTrainSeat.getCarriageSeatIndex());
getSeatList.add(nextDailyTrainSeat);
} else {
LOG.info("座位{}不可选", nextDailyTrainSeat.getCarriageSeatIndex());
isGetAllOffsetSeat = false;
break;
}
}
}
如果所有偏移值对应的座位都成功选定,则将这些座位添加到最终的选座列表 finalSeatList 中,并结束当前车厢的遍历,直接返回。
if (!isGetAllOffsetSeat) {
getSeatList = new ArrayList<>();
continue;
}
finalSeatList.addAll(getSeatList);
return;
calSell 方法
private boolean calSell(DailyTrainSeat dailyTrainSeat, Integer startIndex, Integer endIndex)
/**
* 计算某座位在区间内是否可卖
* 例:sell=10001,本次购买区间站1~4,则区间已售000
* 全部是0,表示这个区间可买;只要有1,就表示区间内已售过票
*
* 选中后,要计算购票后的sell,比如原来是10001,本次购买区间站1~4
* 方案:构造本次购票造成的售卖信息01110,和原sell 10001按位或,最终得到11111
*/
sell 是一个字符串,表示座位的售票信息。每个字符(0或1)代表座位在某个车站区间的售出状态,0表示未售出,1表示已售出。
String sell = dailyTrainSeat.getSell();
sellPart 提取了从 startIndex 到 endIndex 之间的售票信息,这部分表示在用户当前选择的起始站到终点站之间,这个座位是否已经售出。
String sellPart = sell.substring(startIndex, endIndex);
如果 sellPart 的数值大于0,说明该区间内的座位已经被售出,返回 false 表示座位不可选。
if (Integer.parseInt(sellPart) > 0) {
LOG.info("座位{}在本次车站区间{}~{}已售过票,不可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex);
return false;
}
如果 sellPart 的数值为0,说明该区间内的座位尚未售出。
将 sellPart 中的所有 ‘0’ 替换为 ‘1’,表示在这个区间内座位被选中售出。
使用 StrUtil.fillBefore 和 StrUtil.fillAfter 对 curSell 进行填充,以确保长度与原始 sell 字符串相同。
LOG.info("座位{}在本次车站区间{}~{}未售过票,可选中该座位", dailyTrainSeat.getCarriageSeatIndex(), startIndex, endIndex);
String curSell = sellPart.replace('0', '1');
curSell = StrUtil.fillBefore(curSell, '0', endIndex);
curSell = StrUtil.fillAfter(curSell, '0', sell.length());
使用按位或(|)操作将当前区间的售票信息(curSell)与之前的售票信息(sell)合并,得到新的售票状态 newSell。
newSell 被设置为座位的最新售票信息。
返回 true 表示座位在当前区间内可选,并且售票信息已更新。
int newSellInt = NumberUtil.binaryToInt(curSell) | NumberUtil.binaryToInt(sell);
String newSell = NumberUtil.getBinaryStr(newSellInt);
newSell = StrUtil.fillBefore(newSell, '0', sell.length());
LOG.info("座位{}被选中,原售票信息:{},车站区间:{}~{},即:{},最终售票信息:{}"
, dailyTrainSeat.getCarriageSeatIndex(), sell, startIndex, endIndex, curSell, newSell);
dailyTrainSeat.setSell(newSell);
return true;
2.4 AfterConfirmOrderService类
afterDoConfirm 方法
public void afterDoConfirm(DailyTrainTicket dailyTrainTicket, List<DailyTrainSeat> finalSeatList, List<ConfirmOrderTicketReq> tickets, ConfirmOrder confirmOrder) throws Exception
/**
* 选中座位后事务处理:
* 座位表修改售卖情况sell;
* 余票详情表修改余票;
* 为会员增加购票记录
* 更新确认订单为成功
*/
遍历最终选定的座位列表 finalSeatList,更新每个座位的售票信息 sell 以及更新时间。
for (int j = 0; j < finalSeatList.size(); j++) {
DailyTrainSeat dailyTrainSeat = finalSeatList.get(j);
DailyTrainSeat seatForUpdate = new DailyTrainSeat();
seatForUpdate.setId(dailyTrainSeat.getId());
seatForUpdate.setSell(dailyTrainSeat.getSell());
seatForUpdate.setUpdateTime(new Date());
dailyTrainSeatMapper.updateByPrimaryKeySelective(seatForUpdate);
}
计算购买座位后的影响区间。这里的逻辑是确定座位售票信息中,哪些区间的售票信息受到了本次购买的影响。
计算这个站卖出去后,影响了哪些站的余票库存
// 影响的库存:本次选座之前没卖过票的,和本次购买的区间有交集的区间
// 假设10个站,本次买4~7站
// 原售:001000001
// 购买:000011100
// 新售:001011101
// 影响:XXX11111X
然后调用 dailyTrainTicketMapperCust.updateCountBySell 方法,更新受影响区间的余票信息。
Integer startIndex = dailyTrainTicket.getStartIndex();
Integer endIndex = dailyTrainTicket.getEndIndex();
char[] chars = seatForUpdate.getSell().toCharArray();
Integer maxStartIndex = endIndex - 1;
Integer minEndIndex = startIndex + 1;
Integer minStartIndex = 0;
for (int i = startIndex - 1; i >= 0; i--) {
char aChar = chars[i];
if (aChar == '1') {
minStartIndex = i + 1;
break;
}
}
Integer maxEndIndex = seatForUpdate.getSell().length();
for (int i = endIndex; i < seatForUpdate.getSell().length(); i++) {
char aChar = chars[i];
if (aChar == '1') {
maxEndIndex = i;
break;
}
}
LOG.info("影响出发站区间:" + minStartIndex + "-" + maxStartIndex);
LOG.info("影响到达站区间:" + minEndIndex + "-" + maxEndIndex);
dailyTrainTicketMapperCust.updateCountBySell(
dailyTrainSeat.getDate(),
dailyTrainSeat.getTrainCode(),
dailyTrainSeat.getSeatType(),
minStartIndex,
maxStartIndex,
minEndIndex,
maxEndIndex);
创建 MemberTicketReq 对象,并填充相关购票信息。
调用会员服务接口 memberFeign.save(memberTicketReq),为会员增加一张车票。
MemberTicketReq memberTicketReq = new MemberTicketReq();
memberTicketReq.setMemberId(confirmOrder.getMemberId());
memberTicketReq.setPassengerId(tickets.get(j).getPassengerId());
memberTicketReq.setPassengerName(tickets.get(j).getPassengerName());
memberTicketReq.setTrainDate(dailyTrainTicket.getDate());
memberTicketReq.setTrainCode(dailyTrainTicket.getTrainCode());
memberTicketReq.setCarriageIndex(dailyTrainSeat.getCarriageIndex());
memberTicketReq.setSeatRow(dailyTrainSeat.getRow());
memberTicketReq.setSeatCol(dailyTrainSeat.getCol());
memberTicketReq.setStartStation(dailyTrainTicket.getStart());
memberTicketReq.setStartTime(dailyTrainTicket.getStartTime());
memberTicketReq.setEndStation(dailyTrainTicket.getEnd());
memberTicketReq.setEndTime(dailyTrainTicket.getEndTime());
memberTicketReq.setSeatType(dailyTrainSeat.getSeatType());
CommonResp<Object> commonResp = memberFeign.save(memberTicketReq);
LOG.info("调用member接口,返回:{}", commonResp);