|
|
@@ -1,11 +1,21 @@
|
|
|
package com.yc.ship.module.product.service.voyage;
|
|
|
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
import cn.hutool.core.date.DatePattern;
|
|
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
|
|
import cn.hutool.core.util.IdUtil;
|
|
|
+import com.alibaba.excel.util.StringUtils;
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.google.common.collect.Lists;
|
|
|
import com.yc.ship.framework.common.pojo.PageResult;
|
|
|
import com.yc.ship.framework.common.util.collection.CollectionUtils;
|
|
|
import com.yc.ship.framework.common.util.object.BeanUtils;
|
|
|
+import com.yc.ship.module.infra.dal.dataobject.logger.SyncTaskDO;
|
|
|
+import com.yc.ship.module.infra.dal.mysql.logger.SyncTaskMapper;
|
|
|
+import com.yc.ship.module.product.api.AgentOrderApi;
|
|
|
+import com.yc.ship.module.product.api.ShipPlatformApi;
|
|
|
+import com.yc.ship.module.product.api.dto.TradeVisitorDTO;
|
|
|
import com.yc.ship.module.product.controller.admin.pricetemplate.vo.*;
|
|
|
import com.yc.ship.module.product.controller.admin.pricevoyage.vo.PriceVoyageSaveReqVO;
|
|
|
import com.yc.ship.module.product.controller.admin.voyage.vo.*;
|
|
|
@@ -17,6 +27,8 @@ import com.yc.ship.module.product.dal.dataobject.priceroommodel.PriceRoomModelDO
|
|
|
import com.yc.ship.module.product.dal.dataobject.priceroommodeltype.PriceRoomModelTypeDO;
|
|
|
import com.yc.ship.module.product.dal.dataobject.pricesinglesetting.PriceSingleSettingDO;
|
|
|
import com.yc.ship.module.product.dal.dataobject.pricespu.PriceSpuDO;
|
|
|
+import com.yc.ship.module.product.dal.dataobject.voyage.OrderVisitorDo;
|
|
|
+import com.yc.ship.module.product.dal.dataobject.voyage.TicketInfoDo;
|
|
|
import com.yc.ship.module.product.dal.dataobject.voyage.VoyageDO;
|
|
|
import com.yc.ship.module.product.dal.dataobject.voyagestock.VoyageStockDO;
|
|
|
import com.yc.ship.module.product.dal.dataobject.voyagestockdetail.VoyageStockDetailDO;
|
|
|
@@ -28,16 +40,28 @@ import com.yc.ship.module.product.dal.mysql.voyagestockdetail.VoyageStockDetailM
|
|
|
import com.yc.ship.module.product.dal.mysql.voyagestockdistribute.VoyageStockDistributeNewMapper;
|
|
|
import com.yc.ship.module.product.enums.UseStatusEnum;
|
|
|
import com.yc.ship.module.product.enums.YesOrNoEnum;
|
|
|
+import com.yc.ship.module.product.framework.config.ShipPlatformProperties;
|
|
|
import com.yc.ship.module.product.framework.lock.ProductRedisKeyConstants;
|
|
|
import com.yc.ship.module.product.service.pricetemplate.PriceTemplateService;
|
|
|
import com.yc.ship.module.product.service.pricevoyage.PriceVoyageService;
|
|
|
+import com.yc.ship.module.product.utils.SM2Utils;
|
|
|
+import com.yc.ship.module.product.utils.TicketTypeUtil;
|
|
|
import com.yc.ship.module.product.utils.VoyageUUCodeUtils;
|
|
|
import com.yc.ship.module.resource.api.ship.ShipApi;
|
|
|
import com.yc.ship.module.resource.api.ship.dto.RoomModelFloorNumDTO;
|
|
|
import com.yc.ship.module.resource.api.ship.dto.ShipRespDTO;
|
|
|
+import com.yc.ship.module.resource.dal.dataobject.dock.ResourceDockDO;
|
|
|
+import com.yc.ship.module.resource.dal.dataobject.roommodel.ResourceRoomModelDO;
|
|
|
+import com.yc.ship.module.resource.dal.dataobject.route.ResourceRouteDO;
|
|
|
+import com.yc.ship.module.resource.dal.mysql.dock.ResourceDockMapper;
|
|
|
+import com.yc.ship.module.resource.dal.mysql.route.ResourceRouteMapper;
|
|
|
+import com.yc.ship.module.resource.service.room.ResourceRoomService;
|
|
|
+import com.yc.ship.module.resource.service.roommodel.ResourceRoomModelService;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.redisson.api.RLock;
|
|
|
import org.redisson.api.RedissonClient;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
|
@@ -45,13 +69,17 @@ import org.springframework.validation.annotation.Validated;
|
|
|
import javax.annotation.Resource;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.time.Duration;
|
|
|
import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
+import java.util.*;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
import static com.yc.ship.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
import static com.yc.ship.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
|
|
@@ -94,9 +122,48 @@ public class VoyageServiceImpl implements VoyageService {
|
|
|
@Resource
|
|
|
private PriceVoyageService priceVoyageService;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private ResourceRouteMapper resourceRouteMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ResourceRoomModelService roomModelService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ResourceDockMapper resourceDockMapper;
|
|
|
@Resource
|
|
|
private VoyageStockLogMapper voyageStockLogMapper;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private StringRedisTemplate stringRedisTemplate;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private VoyageAsyncService voyageAsyncService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ShipPlatformProperties shipPlatformProperties;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ResourceRoomService roomService;
|
|
|
+
|
|
|
+ @Value("${agent.platform.username}")
|
|
|
+ private String agentUsername;
|
|
|
+ @Value("${agent.platform.password}")
|
|
|
+ private String agentPassword;
|
|
|
+
|
|
|
+ // 公钥
|
|
|
+ @Value("${sm2.public-key}")
|
|
|
+ private String sm2PublicKey;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private SyncTaskMapper syncTaskMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ShipPlatformApi shipPlatformApi;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private AgentOrderApi agentOrderApi;
|
|
|
+
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public Long createVoyage(VoyageSaveReqVO createReqVO) {
|
|
|
@@ -528,4 +595,653 @@ public class VoyageServiceImpl implements VoyageService {
|
|
|
return voyageMapper.countBatchByVoyageIds(voyageIds);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public void syncVoyage(Long id) {
|
|
|
+ VoyageDO voyageDO = voyageMapper.selectById(id);
|
|
|
+ log.info("开始同步航次,id: {}", id);
|
|
|
+ String planId = null;
|
|
|
+ String finalStatus = "FAILED";
|
|
|
+ String errorSummary = null;
|
|
|
+ LocalDateTime taskEndTime;
|
|
|
+ int taskCost;
|
|
|
+
|
|
|
+ // 标记是否为新增操作(用于失败时决定是否清理外部航次)
|
|
|
+ boolean isAddOperation = false;
|
|
|
+ // 判断是否已经同步成功(存在 externalPlanId 且状态为成功)
|
|
|
+ boolean alreadySynced = StringUtils.isNotBlank(voyageDO.getExternalPlanId())
|
|
|
+ && Integer.valueOf(1).equals(voyageDO.getExternalApiStatus());
|
|
|
+
|
|
|
+ // 1. 创建任务主记录
|
|
|
+ SyncTaskDO task = new SyncTaskDO();
|
|
|
+ task.setVoyageId(id);
|
|
|
+ task.setTaskStatus("PROCESSING");
|
|
|
+ task.setStartTime(LocalDateTime.now());
|
|
|
+ task.setCreateTime(LocalDateTime.now());
|
|
|
+ syncTaskMapper.insert(task);
|
|
|
+
|
|
|
+ String validToken = null;
|
|
|
+ String agentToken = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ validToken = getValidToken(task.getId());
|
|
|
+ agentToken = getAgentToken(task.getId());
|
|
|
+ log.info("获取船平台token成功{}-{},taskId: {}", validToken, agentToken, task.getId());
|
|
|
+ if (alreadySynced) {
|
|
|
+ log.info("航次已同步,执行更新操作,planId: {}", voyageDO.getExternalPlanId());
|
|
|
+ planId = voyageDO.getExternalPlanId(); // 使用已有的 planId
|
|
|
+// 禁用航次
|
|
|
+ shipPlatformApi.batchDisablePlan(task.getId(), Lists.newArrayList(planId), validToken);
|
|
|
+ log.info("禁用航次计划成功,planId: {}", planId);
|
|
|
+
|
|
|
+ JSONObject jsonModShip = batchModVoyages(planId, voyageDO);
|
|
|
+ // 2.1 修改航次计划
|
|
|
+ boolean updateResult = shipPlatformApi.modifyVoyage(task.getId(), jsonModShip, validToken);
|
|
|
+ log.info("修改航次计划结果: {}, planId: {}", updateResult, planId);
|
|
|
+
|
|
|
+ // 2.2 修改代理商订单
|
|
|
+ List<TradeVisitorDTO> visitors = voyageMapper.getLocalVisitorsByVoyageId(id);
|
|
|
+ log.info("查询到乘客数量: {}", visitors != null ? visitors.size() : 0);
|
|
|
+ // 分类乘客:已同步的和未同步的
|
|
|
+ List<TradeVisitorDTO> syncedVisitors = new ArrayList<>();
|
|
|
+ List<TradeVisitorDTO> unsyncedVisitors = new ArrayList<>();
|
|
|
+
|
|
|
+ for (TradeVisitorDTO visitor : visitors) {
|
|
|
+ if (!StringUtils.isEmpty(visitor.getExternalPassengerIdApi())) {
|
|
|
+ syncedVisitors.add(visitor);
|
|
|
+ } else {
|
|
|
+ unsyncedVisitors.add(visitor);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("已同步乘客数量: {}, 未同步乘客数量: {}", syncedVisitors.size(), unsyncedVisitors.size());
|
|
|
+ // 2.2.1 异步修改已同步的乘客信息
|
|
|
+ if (!syncedVisitors.isEmpty()) {
|
|
|
+ voyageAsyncService.asyncModifySyncedVisitors(syncedVisitors, task.getId(), agentToken);
|
|
|
+ log.info("已提交异步任务处理 {} 个已同步乘客", syncedVisitors.size());
|
|
|
+ }
|
|
|
+ // 修改完成后设置航次可售
|
|
|
+ shipPlatformApi.batchEnablePlan(task.getId(), Lists.newArrayList(planId), validToken);
|
|
|
+ // 2.2.2 为未同步的乘客创建新订单
|
|
|
+ if (!unsyncedVisitors.isEmpty()) {
|
|
|
+ processUnsyncedVisitors(unsyncedVisitors, planId, id, task.getId(), agentToken);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ log.info("航次未同步,执行新增操作");
|
|
|
+ isAddOperation = true;
|
|
|
+ JSONObject jsonObject = batchAddVoyages(voyageDO);
|
|
|
+ JSONObject jsonObjectAgent = JSONObject.parseObject(jsonObject.toJSONString());
|
|
|
+ removePersonCount(jsonObject);
|
|
|
+
|
|
|
+ planId = shipPlatformApi.addVoyage(task.getId(), shipPlatformProperties.getProductId(voyageDO.getShipId()), jsonObject, validToken);
|
|
|
+ log.info("添加航次计划成功,planId: {}", planId);
|
|
|
+ if (StringUtils.isBlank(planId)) {
|
|
|
+ throw new RuntimeException("添加航次计划返回的planId为空 ");
|
|
|
+ }
|
|
|
+ // 调用代理商api 传入用户信息
|
|
|
+ JSONObject jsonOrder = buildOrder(jsonObjectAgent, planId, id);
|
|
|
+ System.out.println("jsonOrder" + jsonOrder);
|
|
|
+ JSONObject bookingOrderReturnJson = agentOrderApi.bookingOrder(task.getId(), jsonOrder, agentToken);
|
|
|
+ log.info("代理商下单成功,planId: {}", planId);
|
|
|
+ if (bookingOrderReturnJson == null) {
|
|
|
+ throw new RuntimeException("代理商返回数据为空 ");
|
|
|
+ }
|
|
|
+
|
|
|
+// 将返回的乘客id 存起来,用于后续的更新
|
|
|
+ JSONArray tickets = bookingOrderReturnJson.getJSONArray("tickets");
|
|
|
+ if (tickets != null && !tickets.isEmpty()) {
|
|
|
+ List<TradeVisitorDTO> updateList = new ArrayList<>();
|
|
|
+ for (int i = 0; i < tickets.size(); i++) {
|
|
|
+ JSONObject ticket = tickets.getJSONObject(i);
|
|
|
+ String externalPassengerId = ticket.getString("externalPassengerId");
|
|
|
+ String orderPassengerId = ticket.getString("orderPassengerId");
|
|
|
+
|
|
|
+ if (StringUtils.isBlank(externalPassengerId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ TradeVisitorDTO dto = new TradeVisitorDTO();
|
|
|
+ dto.setId(Long.valueOf(externalPassengerId));
|
|
|
+ dto.setExternalPassengerIdApi(orderPassengerId);
|
|
|
+ updateList.add(dto);
|
|
|
+ }
|
|
|
+ if (!updateList.isEmpty()) {
|
|
|
+ voyageMapper.batchUpdateExternalPassengerId(updateList);
|
|
|
+ log.info("批量更新乘客外部ID成功,数量: {}", updateList.size());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 两个操作全部成功后,写入 planId ==========
|
|
|
+ VoyageDO updateDO = new VoyageDO();
|
|
|
+ updateDO.setId(id);
|
|
|
+ updateDO.setExternalPlanId(planId);
|
|
|
+ updateDO.setExternalApiStatus(1);
|
|
|
+ updateDO.setBookingOrderReturnJson(bookingOrderReturnJson.toJSONString());
|
|
|
+ updateDO.setShipIsSync(1);
|
|
|
+ voyageMapper.updateById(updateDO);
|
|
|
+ // ============================================================
|
|
|
+ log.info("api同步返回的信息 同步本地 更新成功,id: {}", id);
|
|
|
+ }
|
|
|
+ finalStatus = "SUCCESS";
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ errorSummary = e.getMessage();
|
|
|
+ finalStatus = "FAILED";
|
|
|
+ log.error("航次同步业务异常,id: {}, planId: {}, error: {}", id, planId, errorSummary, e);
|
|
|
+ compensateOnError(isAddOperation, planId, task,validToken);
|
|
|
+ throw new RuntimeException("航次同步业务异常: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ // 6. 更新任务主记录
|
|
|
+ taskEndTime = LocalDateTime.now();
|
|
|
+ taskCost = (int) Duration.between(task.getStartTime(), taskEndTime).toMillis();
|
|
|
+ task.setEndTime(taskEndTime);
|
|
|
+ task.setCostMillis(taskCost);
|
|
|
+ task.setTaskStatus(finalStatus);
|
|
|
+ task.setErrorSummary(errorSummary);
|
|
|
+ syncTaskMapper.updateById(task);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getValidToken(Long taskId) throws Exception {
|
|
|
+ String token = stringRedisTemplate.opsForValue().get("ship:platform:token");
|
|
|
+ if (StringUtils.isBlank(token)) {
|
|
|
+ token = shipPlatformApi.login(taskId, shipPlatformProperties.getUsername(), shipPlatformProperties.getPassword());
|
|
|
+ stringRedisTemplate.opsForValue().set("ship:platform:token", token, 10, TimeUnit.MINUTES);
|
|
|
+ }
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getAgentToken(Long taskId) throws Exception {
|
|
|
+ String token = stringRedisTemplate.opsForValue().get("agent:platform:token");
|
|
|
+ if (StringUtils.isBlank(token)) {
|
|
|
+ token = agentOrderApi.login(taskId, agentUsername, agentPassword);
|
|
|
+ stringRedisTemplate.opsForValue().set("agent:platform:token", token, 10, TimeUnit.MINUTES);
|
|
|
+ }
|
|
|
+ return token;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据本地航次数据构建外部平台所需的 JSON 参数
|
|
|
+ */
|
|
|
+ public JSONObject batchAddVoyages(VoyageDO voyageDO) {
|
|
|
+ log.info("开始构建航次新增请求,voyageId: {}, shipId: {}", voyageDO.getId(), voyageDO.getShipId());
|
|
|
+ ResourceRouteDO resourceRouteDO = resourceRouteMapper.selectById(voyageDO.getRouteId());
|
|
|
+ log.info("航线信息: {}", resourceRouteDO);
|
|
|
+ JSONObject plan = new JSONObject();
|
|
|
+ plan.put("departureDate", voyageDO.getBoardingTime()); //改为登船时间
|
|
|
+ plan.put("arriveDate", voyageDO.getLeaveTime()); //预计抵达时间
|
|
|
+ plan.put("activeStatus", "enable");//航次销售状态:启用enable;禁用 disable
|
|
|
+ plan.put("stopSeller", 30); // 提前30分钟停止售票
|
|
|
+ plan.put("total", 650); // 总库存
|
|
|
+ plan.put("enlargeTotal", 200); // 放大库存
|
|
|
+ plan.put("stockWarning", 100); // 库存警告
|
|
|
+ plan.put("bookingPrice", 100); // 预订价格
|
|
|
+ plan.put("crossedPrice", 100); // 划线价
|
|
|
+
|
|
|
+ String cruiseCode = shipPlatformProperties.getCruiseCode(voyageDO.getShipId());
|
|
|
+ /* 正式环境在启用*/
|
|
|
+ plan.put("cruiseCode", cruiseCode); // 邮轮ID
|
|
|
+ Map<Long, ResourceDockDO> dockMap = resourceDockMapper.selectBatchIds(
|
|
|
+ Arrays.asList(resourceRouteDO.getOnDockId(), resourceRouteDO.getLeaveDockId())
|
|
|
+ ).stream().collect(Collectors.toMap(ResourceDockDO::getId, d -> d));
|
|
|
+ log.info("码头映射信息: {}", dockMap);
|
|
|
+ plan.put("embarkPort", String.valueOf(dockMap.get(resourceRouteDO.getOnDockId()).getExternalDockId()));
|
|
|
+ plan.put("debarkPort", String.valueOf(dockMap.get(resourceRouteDO.getLeaveDockId()).getExternalDockId()));
|
|
|
+ plan.put("deptCode", resourceRouteDO.getStartPortId()); // 出发港口ID
|
|
|
+ plan.put("destCode", resourceRouteDO.getEndPortId()); // 抵达港口ID
|
|
|
+ plan.put("shipRouteId", resourceRouteDO.getExternalRouteId()); // 航线ID
|
|
|
+
|
|
|
+ // 航次中途港口
|
|
|
+ JSONArray planPorts = new JSONArray();
|
|
|
+ plan.put("planPorts", planPorts);
|
|
|
+
|
|
|
+ // 航次房型数据
|
|
|
+ // 1. 获取该游轮的所有房型
|
|
|
+ List<ResourceRoomModelDO> roomModelList = roomModelService.getRoomModelListByShipId(String.valueOf(voyageDO.getShipId()));
|
|
|
+ log.info("房型列表数量: {}", roomModelList != null ? roomModelList.size() : 0);
|
|
|
+ JSONArray roomTypes = new JSONArray();
|
|
|
+
|
|
|
+ // 2. 遍历每种房型,查询票种价格
|
|
|
+ for (ResourceRoomModelDO roomModel : roomModelList) {
|
|
|
+
|
|
|
+ // 3. 查询该房型+航次的票种价格
|
|
|
+ List<TicketInfoDo> ticketInfoList = voyageMapper.getTicketInfoByRoomType(
|
|
|
+ voyageDO.getId(),
|
|
|
+ String.valueOf(roomModel.getId())
|
|
|
+ );
|
|
|
+ System.out.println("票价查询参数:" + voyageDO.getId() + " " + roomModel.getId());
|
|
|
+ System.out.println("ticketInfoList" + ticketInfoList);
|
|
|
+
|
|
|
+ if (CollUtil.isNotEmpty(ticketInfoList)) {
|
|
|
+ JSONObject room = new JSONObject();
|
|
|
+ room.put("roomTypeCode", String.valueOf(roomModel.getId())); // 房型ID作为代码
|
|
|
+ room.put("roomTypeName", roomModel.getName()); // 房型名称
|
|
|
+ room.put("status", "enable");
|
|
|
+ JSONArray ticketPrices = new JSONArray();
|
|
|
+ for (TicketInfoDo info : ticketInfoList) {
|
|
|
+ JSONObject ticket = new JSONObject();
|
|
|
+ ticket.put("ticketTypeName", info.getTicketTypeName());
|
|
|
+ ticket.put("childFlag", info.getChildFlag() == 1);
|
|
|
+ ticket.put("publicPrice", 4999);
|
|
|
+ ticket.put("price", 4999);
|
|
|
+ ticket.put("couponFlag", false);
|
|
|
+ ticket.put("stockConsumeFlag", 1);
|
|
|
+ ticket.put("status", "enable");
|
|
|
+ ticket.put("personCount", info.getPersonCount());
|
|
|
+ ticketPrices.add(ticket);
|
|
|
+ }
|
|
|
+ room.put("ticketTypePriceAddVos", ticketPrices);
|
|
|
+ roomTypes.add(room);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ System.out.println("roomTypes" + roomTypes);
|
|
|
+ // 校验:确保至少有一个有效房型
|
|
|
+ if (roomTypes.isEmpty()) {
|
|
|
+ throw new RuntimeException("该航次未配置任何房型的票价数据,请先配置");
|
|
|
+ }
|
|
|
+
|
|
|
+ plan.put("voyageRoomTypeAddVos", roomTypes);
|
|
|
+ log.info("航次新增请求构建完成");
|
|
|
+ System.out.println("plan" + plan);
|
|
|
+ return plan;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据本地航次数据构建外部平台所需的 JSON 参数--修改航次
|
|
|
+ */
|
|
|
+ public JSONObject batchModVoyages(String planId, VoyageDO voyageDO) {
|
|
|
+ // 1. 查询航线信息(用于获取港口、码头等)
|
|
|
+ ResourceRouteDO resourceRouteDO = resourceRouteMapper.selectById(voyageDO.getRouteId());
|
|
|
+
|
|
|
+ JSONObject modifyRequest = new JSONObject();
|
|
|
+ // 航次ID
|
|
|
+ modifyRequest.put("planId", planId);
|
|
|
+ // 航次出发日期 / 预计抵达时间
|
|
|
+ modifyRequest.put("departureDate", voyageDO.getStartTime()); // yyyy-MM-dd HH:mm:ss
|
|
|
+ modifyRequest.put("arriveDate", voyageDO.getLeaveTime()); // yyyy-MM-dd HH:mm:ss
|
|
|
+ // 航次销售状态(启用/禁用)
|
|
|
+ modifyRequest.put("activeStatus", "disable");
|
|
|
+ // 提前停止售票(分钟)
|
|
|
+ modifyRequest.put("stopSeller", 30);
|
|
|
+ // 总库存 / 放大库存 / 库存警告
|
|
|
+ modifyRequest.put("total", 650);
|
|
|
+ modifyRequest.put("enlargeTotal", 200);
|
|
|
+ modifyRequest.put("stockWarning", 100);
|
|
|
+ // 预订价格 / 划线价
|
|
|
+ modifyRequest.put("bookingPrice", 100);
|
|
|
+ modifyRequest.put("crossedPrice", 100);
|
|
|
+
|
|
|
+ // 港口码头信息(从航线中获取)
|
|
|
+ /* 正式环境在启用*/
|
|
|
+// modifyRequest.put("cruiseCode", String.valueOf(voyageDO.getShipId())); // 邮轮ID
|
|
|
+ Map<Long, ResourceDockDO> dockMap = resourceDockMapper.selectBatchIds(
|
|
|
+ Arrays.asList(resourceRouteDO.getOnDockId(), resourceRouteDO.getLeaveDockId())
|
|
|
+ ).stream().collect(Collectors.toMap(ResourceDockDO::getId, d -> d));
|
|
|
+
|
|
|
+ modifyRequest.put("embarkPort", String.valueOf(dockMap.get(resourceRouteDO.getOnDockId()).getExternalDockId()));
|
|
|
+ modifyRequest.put("debarkPort", String.valueOf(dockMap.get(resourceRouteDO.getLeaveDockId()).getExternalDockId()));
|
|
|
+ modifyRequest.put("deptCode", resourceRouteDO.getStartPortId()); // 出发港口ID
|
|
|
+ modifyRequest.put("destCode", resourceRouteDO.getEndPortId()); // 抵达港口ID
|
|
|
+ modifyRequest.put("shipRouteId", resourceRouteDO.getExternalRouteId()); // 航线ID
|
|
|
+ // 附件IDs
|
|
|
+ modifyRequest.put("fileIds", "");
|
|
|
+
|
|
|
+ // 2. 航次中途港口
|
|
|
+ JSONArray planPorts = new JSONArray();
|
|
|
+ modifyRequest.put("planPorts", planPorts);
|
|
|
+
|
|
|
+ // 3. 航次房型数据
|
|
|
+ // 获取该游轮的所有房型
|
|
|
+ List<ResourceRoomModelDO> roomModelList = roomModelService.getRoomModelListByShipId(String.valueOf(voyageDO.getShipId()));
|
|
|
+ System.out.println("roomModelList:" + roomModelList);
|
|
|
+
|
|
|
+ JSONArray roomTypes = new JSONArray();
|
|
|
+ for (ResourceRoomModelDO roomModel : roomModelList) {
|
|
|
+
|
|
|
+
|
|
|
+ // 4. 查询该房型 + 当前航次下的票种价格
|
|
|
+ List<TicketInfoDo> ticketInfoList = voyageMapper.getTicketInfoByRoomType(
|
|
|
+ voyageDO.getId(),
|
|
|
+ String.valueOf(roomModel.getId())
|
|
|
+ );
|
|
|
+ System.out.println("票价查询参数:planId=" + planId + ", roomId=" + roomModel.getId());
|
|
|
+ System.out.println("ticketInfoList:" + ticketInfoList);
|
|
|
+
|
|
|
+
|
|
|
+ if (CollUtil.isNotEmpty(ticketInfoList)) {
|
|
|
+ JSONObject room = new JSONObject();
|
|
|
+ room.put("roomTypeCode", String.valueOf(roomModel.getId()));
|
|
|
+ room.put("roomTypeName", roomModel.getName());
|
|
|
+ room.put("status", "enable");
|
|
|
+
|
|
|
+ JSONArray ticketPrices = new JSONArray();
|
|
|
+ for (TicketInfoDo info : ticketInfoList) {
|
|
|
+ JSONObject ticket = new JSONObject();
|
|
|
+ ticket.put("ticketTypeName", info.getTicketTypeName());
|
|
|
+ ticket.put("childFlag", info.getChildFlag() == 1);
|
|
|
+ ticket.put("publicPrice", 4999);
|
|
|
+ ticket.put("price", 4999);
|
|
|
+ ticket.put("couponFlag", false);
|
|
|
+ ticket.put("stockConsumeFlag", true);
|
|
|
+ ticket.put("status", "enable");
|
|
|
+ ticketPrices.add(ticket);
|
|
|
+ }
|
|
|
+ room.put("ticketTypePriceModifyVos", ticketPrices);
|
|
|
+ roomTypes.add(room);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ modifyRequest.put("voyageRoomTypeModifyVos", roomTypes);
|
|
|
+ // 如果所有房型都无票种
|
|
|
+ if (roomTypes.isEmpty()) {
|
|
|
+ throw new RuntimeException("该航次没有可修改的房型票价数据,请先配置票种");
|
|
|
+ }
|
|
|
+ System.out.println("修改请求体:" + modifyRequest);
|
|
|
+ return modifyRequest;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建用户预定订单信息为api格式
|
|
|
+ */
|
|
|
+ private JSONObject buildOrder(JSONObject jsonObjectAgent, String planId, Long voyageId) {
|
|
|
+ JSONObject order = new JSONObject();
|
|
|
+ // 1. 联系人
|
|
|
+ JSONObject contactMan = new JSONObject();
|
|
|
+ //获取航向
|
|
|
+ String voyageDirection = voyageMapper.getVoyageDirectionByVoyageId(voyageId);
|
|
|
+ if ("1".equals(voyageDirection)) {
|
|
|
+ contactMan.put("name", "罗利娅");
|
|
|
+ contactMan.put("phone", "15717896534");
|
|
|
+ } else {
|
|
|
+ contactMan.put("name", "汪子祎");
|
|
|
+ contactMan.put("phone", "18580594535");
|
|
|
+ }
|
|
|
+ order.put("contactMan", contactMan);
|
|
|
+
|
|
|
+ // 2. 航次信息
|
|
|
+ JSONArray cruisePlans = new JSONArray();
|
|
|
+ JSONObject cruisePlan = new JSONObject();
|
|
|
+ // 航次添加后返回的planid
|
|
|
+// cruisePlan.put("cruisePlanId", "305898028676415488");
|
|
|
+ cruisePlan.put("cruisePlanId", planId);
|
|
|
+
|
|
|
+ // 3. planstocks 数组
|
|
|
+ JSONArray planstocks = new JSONArray();
|
|
|
+ JSONArray voyageRoomTypeAddVos = jsonObjectAgent.getJSONArray("voyageRoomTypeAddVos");
|
|
|
+
|
|
|
+ for (int i = 0; i < voyageRoomTypeAddVos.size(); i++) {
|
|
|
+ JSONObject roomType = voyageRoomTypeAddVos.getJSONObject(i);
|
|
|
+ String roomTypeName = roomType.getString("roomTypeName");
|
|
|
+ JSONArray ticketTypes = roomType.getJSONArray("ticketTypePriceAddVos");
|
|
|
+
|
|
|
+ for (int j = 0; j < ticketTypes.size(); j++) {
|
|
|
+ JSONObject ticket = ticketTypes.getJSONObject(j);
|
|
|
+ int personCount = ticket.getIntValue("personCount");
|
|
|
+ if (personCount <= 0) {
|
|
|
+ continue; // 跳过无人数的票种
|
|
|
+ }
|
|
|
+ String ticketTypeName = ticket.getString("ticketTypeName");
|
|
|
+
|
|
|
+ JSONObject planstock = new JSONObject();
|
|
|
+ planstock.put("num", personCount);
|
|
|
+ planstock.put("ticketName", ticketTypeName);
|
|
|
+ planstock.put("roomTypeName", roomTypeName);
|
|
|
+
|
|
|
+ // 生成 passengers 数组,根据房型 航号,票型 查找对应的乘客数据
|
|
|
+ List<String> dbTypes = TicketTypeUtil.toDbTypes(ticketTypeName, roomTypeName, voyageId);
|
|
|
+ System.out.println("dbTypes" + dbTypes);
|
|
|
+ List<OrderVisitorDo> passengerData = voyageMapper.getPassengerData(voyageId, dbTypes, roomTypeName);
|
|
|
+ System.out.println("ticketTypeName" + ticketTypeName);
|
|
|
+ System.out.println("passengerData" + passengerData);
|
|
|
+ if (passengerData == null || passengerData.isEmpty()) {
|
|
|
+ // 没有乘客数据,跳过该 planstock
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ JSONArray passengers = new JSONArray();
|
|
|
+ int no = 1;
|
|
|
+ for (OrderVisitorDo visitor : passengerData) {
|
|
|
+// if (!StringUtils.isEmpty(visitor.getExternalPassengerIdApi())) {
|
|
|
+// log.info("用户:{}, 外部ID:{} 已经同步过,忽略...", visitor.getId(), visitor.getExternalPassengerIdApi());
|
|
|
+// continue;
|
|
|
+// }
|
|
|
+ JSONObject passenger = new JSONObject();
|
|
|
+ String name = visitor.getName();
|
|
|
+ if (name != null) {
|
|
|
+ name = name.replaceAll("^\\p{javaWhitespace}+|\\p{javaWhitespace}+$", "");
|
|
|
+ }
|
|
|
+ passenger.put("name", name);
|
|
|
+ String certNo = visitor.getCertNo();
|
|
|
+ if (certNo == null || certNo.trim().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("旅客证件号不能为空,旅客姓名:" + visitor.getName());
|
|
|
+ }
|
|
|
+ passenger.put("certNo", SM2Utils.encrypt(certNo, sm2PublicKey)); // 需SM2加密
|
|
|
+ passenger.put("phone", visitor.getPhone()); // 手机号
|
|
|
+ passenger.put("age",visitor.getAge()); // 年龄
|
|
|
+ passenger.put("isTransfer", visitor.getIsTransfer() == 1);// 是否接送
|
|
|
+
|
|
|
+ int sex = visitor.getSex() == null ? 1 : visitor.getSex();
|
|
|
+ passenger.put("sex", sex == 1 ? "male" : "female");
|
|
|
+ passenger.put("category", visitor.getCategory()); // 类别:aduit/kid
|
|
|
+ passenger.put("countryId", visitor.getCountryId()); // 国家ID
|
|
|
+ passenger.put("certType", visitor.getCertType()); // 证件类型
|
|
|
+ passenger.put("gobackFlag", false);
|
|
|
+ String[] split = visitor.getOrderNo().split("-");
|
|
|
+ passenger.put("groupSortNo", split[split.length - 1]);
|
|
|
+ passenger.put("no", no++);
|
|
|
+ passenger.put("externalPassengerId", visitor.getId()); //游客id
|
|
|
+ passengers.add(passenger);
|
|
|
+ }
|
|
|
+ planstock.put("passengers", passengers);
|
|
|
+ planstocks.add(planstock);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ cruisePlan.put("planstocks", planstocks);
|
|
|
+ cruisePlans.add(cruisePlan);
|
|
|
+ order.put("cruisePlans", cruisePlans);
|
|
|
+
|
|
|
+ // 4. 三方订单号
|
|
|
+ order.put("externalOrderId", "" + System.currentTimeMillis());
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 构建单个乘客的修改请求
|
|
|
+ private JSONObject buildModOrder(TradeVisitorDTO visitor) {
|
|
|
+ JSONObject modifyData = new JSONObject();
|
|
|
+ modifyData.put("orderPassengerId", visitor.getExternalPassengerIdApi());
|
|
|
+ modifyData.put("name", visitor.getName());
|
|
|
+ modifyData.put("certType", visitor.getCertType());
|
|
|
+ modifyData.put("certNo", SM2Utils.encrypt(visitor.getCredentialNo(), sm2PublicKey));
|
|
|
+ modifyData.put("countryId", visitor.getNationalityId());
|
|
|
+ modifyData.put("phone", visitor.getMobile());
|
|
|
+
|
|
|
+ int sex = visitor.getGender() == null ? 1 : visitor.getGender();
|
|
|
+ modifyData.put("sex", sex == 1 ? "male" : "female");
|
|
|
+ modifyData.put("age", visitor.getAge());
|
|
|
+ modifyData.put("category", visitor.getCategory());
|
|
|
+ return modifyData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 递归移除 JSON 中的 personCount 字段
|
|
|
+ */
|
|
|
+ private void removePersonCount(Object obj) {
|
|
|
+ if (obj instanceof JSONObject) {
|
|
|
+ JSONObject json = (JSONObject) obj;
|
|
|
+ json.remove("personCount");
|
|
|
+ for (Object value : json.values()) {
|
|
|
+ removePersonCount(value);
|
|
|
+ }
|
|
|
+ } else if (obj instanceof JSONArray) {
|
|
|
+ JSONArray array = (JSONArray) obj;
|
|
|
+ for (int i = 0; i < array.size(); i++) {
|
|
|
+ removePersonCount(array.get(i));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 补偿操作:当航次同步失败时,清理已创建的外部航次
|
|
|
+ *
|
|
|
+ * @param isAddOperation 是否为新增操作
|
|
|
+ * @param planId 外部航次ID
|
|
|
+ * @param task 同步任务记录
|
|
|
+ */
|
|
|
+ private void compensateOnError(boolean isAddOperation, String planId, SyncTaskDO task,String validToken) {
|
|
|
+ if (isAddOperation && StringUtils.isNotBlank(planId)) {
|
|
|
+ try {
|
|
|
+ log.info("开始执行补偿操作,清理外部航次,planId: {}", planId);
|
|
|
+ String compensateToken = getValidToken(task.getId());
|
|
|
+ List<String> delList = new ArrayList<>();
|
|
|
+ delList.add(planId);
|
|
|
+ boolean deleteResult = shipPlatformApi.deleteVoyages(task.getId(), delList, compensateToken);
|
|
|
+ if (deleteResult) {
|
|
|
+ log.info("航次同步失败后清理成功,planId: {}", planId);
|
|
|
+ } else {
|
|
|
+ log.error("航次同步失败后清理失败,planId: {},需要人工介入处理", planId);
|
|
|
+ }
|
|
|
+ } catch (Exception deleteException) {
|
|
|
+ log.error("航次同步失败后清理异常,planId: {},需要人工介入处理", planId, deleteException);
|
|
|
+ }
|
|
|
+ } else if (!isAddOperation && StringUtils.isNotBlank(planId)) {
|
|
|
+ try {
|
|
|
+ log.info("修改操作失败,恢复航次为开启状态,planId: {}", planId);
|
|
|
+ shipPlatformApi.batchEnablePlan(task.getId(), Lists.newArrayList(planId), validToken);
|
|
|
+ log.info("航次状态恢复成功,planId: {}", planId);
|
|
|
+ } catch (Exception enableException) {
|
|
|
+ log.error("航次状态恢复异常,planId: {},需要人工介入处理", planId, enableException);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.warn("航次同步失败,跳过清理操作,isAddOperation: {}, planId: {}", isAddOperation, planId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理未同步的乘客,为他们创建新订单
|
|
|
+ */
|
|
|
+ private void processUnsyncedVisitors(List<TradeVisitorDTO> unsyncedVisitors, String planId,
|
|
|
+ Long voyageId, Long taskId, String agentToken) {
|
|
|
+ log.info("开始为 {} 个未同步乘客创建新订单", unsyncedVisitors.size());
|
|
|
+ JSONObject order = new JSONObject();
|
|
|
+ // 1. 联系人
|
|
|
+ JSONObject contactMan = new JSONObject();
|
|
|
+ //获取航向
|
|
|
+ String voyageDirection = voyageMapper.getVoyageDirectionByVoyageId(voyageId);
|
|
|
+ if ("1".equals(voyageDirection)) {
|
|
|
+ contactMan.put("name", "罗利娅");
|
|
|
+ contactMan.put("phone", "15717896534");
|
|
|
+ } else {
|
|
|
+ contactMan.put("name", "汪子祎");
|
|
|
+ contactMan.put("phone", "18580594535");
|
|
|
+ }
|
|
|
+ order.put("contactMan", contactMan);
|
|
|
+
|
|
|
+ // 2. 航次信息
|
|
|
+ JSONArray cruisePlans = new JSONArray();
|
|
|
+ JSONObject cruisePlan = new JSONObject();
|
|
|
+ // 航次添加后返回的planid
|
|
|
+ cruisePlan.put("cruisePlanId", planId);
|
|
|
+ // 3. 按房型和票型分组处理未同步乘客
|
|
|
+ JSONArray planstocks = new JSONArray();
|
|
|
+ // 按房型名称和票型类型分组
|
|
|
+ Map<String, List<TradeVisitorDTO>> groupedVisitors = unsyncedVisitors.stream()
|
|
|
+ .filter(visitor -> visitor.getRoomModelName() != null && visitor.getType() != null)
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
+ visitor -> visitor.getRoomModelName() + "_" + visitor.getType()
|
|
|
+ ));
|
|
|
+
|
|
|
+ // 为每个分组构建 planstock
|
|
|
+ for (Map.Entry<String, List<TradeVisitorDTO>> entry : groupedVisitors.entrySet()) {
|
|
|
+ String[] keys = entry.getKey().split("_");
|
|
|
+ String roomTypeName = keys[0];
|
|
|
+ String ticketTypeName = keys[1];
|
|
|
+ List<TradeVisitorDTO> visitors = entry.getValue();
|
|
|
+
|
|
|
+ if (visitors.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject planstock = new JSONObject();
|
|
|
+ planstock.put("num", visitors.size());
|
|
|
+ planstock.put("ticketName", ticketTypeName);
|
|
|
+ planstock.put("roomTypeName", roomTypeName);
|
|
|
+
|
|
|
+ // 构建乘客列表
|
|
|
+ JSONArray passengers = new JSONArray();
|
|
|
+ int no = 1;
|
|
|
+ for (TradeVisitorDTO visitor : visitors) {
|
|
|
+ JSONObject passenger = new JSONObject();
|
|
|
+ String name = visitor.getName();
|
|
|
+ if (name != null) {
|
|
|
+ name = name.replaceAll("^\\p{javaWhitespace}+|\\p{javaWhitespace}+$", "");
|
|
|
+ }
|
|
|
+ passenger.put("name", name);
|
|
|
+ String certNo = visitor.getCredentialNo();
|
|
|
+ if (certNo == null || certNo.trim().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("旅客证件号不能为空,旅客姓名:" + visitor.getName());
|
|
|
+ }
|
|
|
+ passenger.put("certNo", SM2Utils.encrypt(certNo, sm2PublicKey)); // 需SM2加密
|
|
|
+ passenger.put("phone", visitor.getMobile()); // 手机号
|
|
|
+ passenger.put("age", visitor.getAge()); // 年龄
|
|
|
+ passenger.put("isTransfer", visitor.getIsTransfer() == 1);// 是否接送
|
|
|
+
|
|
|
+
|
|
|
+ int sex = visitor.getGender() == null ? 1 : visitor.getGender();
|
|
|
+ passenger.put("sex", sex == 1 ? "male" : "female");
|
|
|
+ passenger.put("category", visitor.getCategory()); // 类别:aduit/kid
|
|
|
+ passenger.put("countryId", visitor.getNationalityId()); // 国家ID
|
|
|
+ passenger.put("certType", visitor.getCertType()); // 证件类型
|
|
|
+ passenger.put("gobackFlag", false);
|
|
|
+ passenger.put("groupSortNo", 1);
|
|
|
+ passenger.put("no", no++);
|
|
|
+ passenger.put("externalPassengerId", visitor.getId()); //游客id
|
|
|
+ passengers.add(passenger);
|
|
|
+
|
|
|
+ }
|
|
|
+ planstock.put("passengers", passengers);
|
|
|
+ planstocks.add(planstock);
|
|
|
+ }
|
|
|
+
|
|
|
+ cruisePlan.put("planstocks", planstocks);
|
|
|
+ cruisePlans.add(cruisePlan);
|
|
|
+ order.put("cruisePlans", cruisePlans);
|
|
|
+ // 4. 三方订单号
|
|
|
+ order.put("externalOrderId", "" + System.currentTimeMillis());
|
|
|
+ log.info("构建完成");;
|
|
|
+
|
|
|
+ System.out.println("processUnsyncedVisitors Order:" + order);
|
|
|
+
|
|
|
+ try {
|
|
|
+ JSONObject bookingOrderReturnJson = agentOrderApi.bookingOrder(taskId, order, agentToken);
|
|
|
+ log.info("代理商下单成功,planId: {}", planId);
|
|
|
+ if (bookingOrderReturnJson == null) {
|
|
|
+ throw new RuntimeException("代理商返回数据为空 ");
|
|
|
+ }
|
|
|
+// 将返回的乘客id 存起来,用于后续的更新
|
|
|
+ JSONArray tickets = bookingOrderReturnJson.getJSONArray("tickets");
|
|
|
+ if (tickets != null && !tickets.isEmpty()) {
|
|
|
+ List<TradeVisitorDTO> updateList = new ArrayList<>();
|
|
|
+ for (int i = 0; i < tickets.size(); i++) {
|
|
|
+ JSONObject ticket = tickets.getJSONObject(i);
|
|
|
+ String externalPassengerId = ticket.getString("externalPassengerId");
|
|
|
+ String orderPassengerId = ticket.getString("orderPassengerId");
|
|
|
+
|
|
|
+ if (StringUtils.isBlank(externalPassengerId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ TradeVisitorDTO dto = new TradeVisitorDTO();
|
|
|
+ dto.setId(Long.valueOf(externalPassengerId));
|
|
|
+ dto.setExternalPassengerIdApi(orderPassengerId);
|
|
|
+ updateList.add(dto);
|
|
|
+ }
|
|
|
+ if (!updateList.isEmpty()) {
|
|
|
+ voyageMapper.batchUpdateExternalPassengerId(updateList);
|
|
|
+ log.info("批量更新乘客外部ID成功,数量: {}", updateList.size());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("代理商下单异常,planId: {}", planId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
}
|