From c02a897876fce43ac3bcc747510e80f44d2f32ed Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Tue, 27 Oct 2026 19:30:31 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=92=E5=BA=97=E4=BF=AE=E6=94=B9=E5=BE=85?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/SmHotelOrderEntity.java | 7 +- .../controller/SmHotelOrderController.java | 24 +- .../service/SmHotelHuidingOrderService.java | 7 + .../service/SmHotelOrderService.java | 12 +- .../service/SmHotelShengnuoOrderService.java | 7 + .../impl/SmHotelHuidingOrderServiceImpl.java | 57 +++- .../service/impl/SmHotelMidServiceImpl.java | 25 +- .../service/impl/SmHotelOrderServiceImpl.java | 250 ++++++++++++++++-- .../impl/SmHotelShengnuoOrderServiceImpl.java | 43 ++- 9 files changed, 397 insertions(+), 35 deletions(-) diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java index 1a8c92f6..8a73e1ea 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java @@ -159,11 +159,16 @@ public class SmHotelOrderEntity extends Model { @Schema(description = "下游应用名称") private String downstreamAppName; - /** 列表展示:酒店数据来源 local/shangyou/其它渠道(非表字段) */ + /** 列表展示:酒店数据来源 local/shangyou/其它渠道(非表字段);查询时可按来源过滤 */ @TableField(exist = false) @Schema(description = "酒店数据来源") private String hotelDataSource; + /** 查询:按当前用户酒店 DataScope 过滤(酒店管家端传 true) */ + @TableField(exist = false) + @Schema(description = "是否按可见酒店范围过滤") + private Boolean scopeByHotel; + /** 列表展示:酒店归属部门(对应商户,结算用;非表字段,来自 canteen_hotel.dept_id) */ @TableField(exist = false) @Schema(description = "酒店归属部门ID") diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java index 18f52ce9..1473ec98 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelOrderController.java @@ -28,7 +28,7 @@ public class SmHotelOrderController { @Operation(summary = "订单分页") @GetMapping("/page") - @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view')") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view') or @pms.hasPermission('supermarket_smHotel_view')") public R> page(@ParameterObject Page page, @ParameterObject SmHotelOrderEntity query) { return R.ok(orderService.pageOrders(page, query)); @@ -36,14 +36,14 @@ public class SmHotelOrderController { @Operation(summary = "订单来源筛选项(渠道下游 + 开放应用)") @GetMapping("/orderSourceOptions") - @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view')") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view') or @pms.hasPermission('supermarket_smHotel_view')") public R>> orderSourceOptions() { return R.ok(orderService.listOrderSourceOptions()); } @Operation(summary = "订单详情(含酒店/产品/日历)") @GetMapping("/{id}") - @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view')") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view') or @pms.hasPermission('supermarket_smHotel_view')") public R detail(@PathVariable Long id) { SmHotelOrderDetailVo vo = orderService.getAdminDetail(id); if (vo == null || vo.getOrder() == null) { @@ -61,6 +61,24 @@ public class SmHotelOrderController { return orderService.cancelByAdmin(id, reason); } + @SysLog("本地酒店订单确认接单") + @Operation(summary = "本地数据源订单人工确认接单") + @PostMapping("/confirm/{id}") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_cancel') or @pms.hasPermission('supermarket_smHotel_edit')") + public R confirm(@PathVariable Long id, @RequestBody(required = false) Map body) { + String reason = body == null ? null : String.valueOf(body.getOrDefault("reason", "人工确认接单")); + return orderService.confirmLocalByAdmin(id, reason); + } + + @SysLog("本地酒店订单拒绝") + @Operation(summary = "本地数据源订单人工拒绝(已支付则退款)") + @PostMapping("/reject/{id}") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_cancel') or @pms.hasPermission('supermarket_smHotel_edit')") + public R reject(@PathVariable Long id, @RequestBody(required = false) Map body) { + String reason = body == null ? null : String.valueOf(body.getOrDefault("reason", "人工拒绝接单")); + return orderService.rejectLocalByAdmin(id, reason); + } + @SysLog("后台退款酒店订单") @Operation(summary = "后台退款:已支付订单原路退款并关闭预订") @PostMapping("/refund/{id}") diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelHuidingOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelHuidingOrderService.java index 270045b4..2355bb78 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelHuidingOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelHuidingOrderService.java @@ -2,6 +2,7 @@ package com.bsd.pjcloud.supermarket.service; import cn.hutool.json.JSONObject; import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity; import java.util.Map; @@ -27,4 +28,10 @@ public interface SmHotelHuidingOrderService { /** 主动查对方订单(联调/排障) */ R queryRemote(String partnerCode, String ordercode, String childcode); + + /** 本地酒店人工确认后回写慧订 operationOrder=1 */ + void notifyLocalOrderConfirmed(SmHotelOrderEntity order); + + /** 本地酒店人工拒绝后回写慧订 operationOrder=2 */ + void notifyLocalOrderRejected(SmHotelOrderEntity order); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java index bf1cbacc..33bd84c1 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java @@ -50,7 +50,7 @@ public interface SmHotelOrderService { SmHotelOrderPayInfoVo getPayInfo(Long orderId); /** - * App 支付成功履约。本地酒店确认已预占库存;上游酒店再调上游下单。 + * App 支付成功履约。本地酒店占库存并待人工确认;上游酒店再调上游下单。 */ Boolean onAppPaySuccess(Long orderId); @@ -91,6 +91,16 @@ public interface SmHotelOrderService { /** PC 后台取消 */ R cancelByAdmin(Long id, String reason); + /** + * 本地数据源订单人工确认接单:PENDING_CONFIRM → UPSTREAM_OK,并通知下游。 + */ + R confirmLocalByAdmin(Long id, String reason); + + /** + * 本地数据源订单人工拒绝:取消并释库存;已支付则原路退款;通知下游拒绝。 + */ + R rejectLocalByAdmin(Long id, String reason); + /** Worker:扫描到期补偿任务 */ int processDueCompensate(int limit); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelShengnuoOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelShengnuoOrderService.java index 6c61f8cf..8344244a 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelShengnuoOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelShengnuoOrderService.java @@ -1,6 +1,7 @@ package com.bsd.pjcloud.supermarket.service; import cn.hutool.json.JSONObject; +import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity; import java.util.Map; @@ -15,4 +16,10 @@ public interface SmHotelShengnuoOrderService { /** 即将确认超时提醒:只记日志,返回成功 */ Map orderConfirmTimeOut(String partnerCode, JSONObject body); + + /** 本地酒店人工确认后 orderChange=已接单(2) */ + void notifyLocalOrderConfirmed(SmHotelOrderEntity order); + + /** 本地酒店人工拒绝后 orderChange=已取消(5) */ + void notifyLocalOrderRejected(SmHotelOrderEntity order); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java index ed82cee7..488a7e3c 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java @@ -208,17 +208,36 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic order.setHuanqiPayload(data.toString()); orderMapper.insert(order); - // 同步订上游:当场返回成败,不走「先待确认再 operationOrder」中间态 + // 本地酒店:占库存后 PENDING_CONFIRM,返回待确认,人工确认后再 operationOrder=1 + // 上游酒店:同步订上游,成功当场确认 R book = hotelOrderService.bookUpstreamForExisting(order.getId()); SmHotelOrderEntity latest = orderMapper.selectById(order.getId()); if (latest == null) { return fail200("下单后订单丢失"); } - if (book == null || book.getCode() != 0 || !isBookConfirmed(latest)) { + if (book == null || book.getCode() != 0) { String failMsg = latest.getFailMsg(); if (StrUtil.isBlank(failMsg)) { failMsg = book == null ? "上游下单失败" : StrUtil.blankToDefault(book.getMsg(), "上游下单失败"); } + if (!"UPSTREAM_FAIL".equals(latest.getOrderStatus()) && !"NEED_MANUAL".equals(latest.getOrderStatus()) + && !"CANCELLED".equals(latest.getOrderStatus()) + && !"PENDING_CONFIRM".equals(latest.getOrderStatus())) { + latest.setOrderStatus("UPSTREAM_FAIL"); + latest.setFailMsg(StrUtil.maxLength(failMsg, 500)); + orderMapper.updateById(latest); + } + pushOperationOrder(partnerCode, latest, OP_REJECT); + return fail200(failMsg); + } + // 本地待确认:创单成功但不推确认 + if (isPendingConfirm(latest)) { + outer.put("msg", "下单成功,待确认"); + outer.put("data", buildCreateData(latest)); + return outer; + } + if (!isBookConfirmed(latest)) { + String failMsg = StrUtil.blankToDefault(latest.getFailMsg(), "上游下单未确认"); if (!"UPSTREAM_FAIL".equals(latest.getOrderStatus()) && !"NEED_MANUAL".equals(latest.getOrderStatus()) && !"CANCELLED".equals(latest.getOrderStatus())) { latest.setOrderStatus("UPSTREAM_FAIL"); @@ -295,10 +314,20 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic .eq(SmHotelOrderEntity::getChannelNo, channelNo) .last("limit 1")); if (exist != null) { + if (isPendingConfirm(exist)) { + resp.put("code", 0); + resp.put("message", "本地订单待确认"); + return resp; + } if (StrUtil.isBlank(exist.getPlatformNo()) && !"CANCELLED".equals(exist.getOrderStatus()) && !isBookConfirmed(exist)) { R book = hotelOrderService.bookUpstreamForExisting(exist.getId()); SmHotelOrderEntity latest = orderMapper.selectById(exist.getId()); + if (latest != null && isPendingConfirm(latest)) { + resp.put("code", 0); + resp.put("message", "本地订单待确认"); + return resp; + } if (book == null || book.getCode() != 0 || latest == null || !isBookConfirmed(latest)) { resp.put("code", 1); resp.put("message", latest != null && StrUtil.isNotBlank(latest.getFailMsg()) ? latest.getFailMsg() @@ -410,9 +439,9 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic } } - /** 上游已明确订成(含本地库存单) */ + /** 上游/人工已明确订成(不含待确认) */ private static boolean isBookConfirmed(SmHotelOrderEntity order) { - if (order == null) { + if (order == null || isPendingConfirm(order)) { return false; } if ("UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus())) { @@ -424,6 +453,26 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic && !"CANCELLED".equals(order.getOrderStatus()); } + private static boolean isPendingConfirm(SmHotelOrderEntity order) { + return order != null && "PENDING_CONFIRM".equals(order.getOrderStatus()); + } + + @Override + public void notifyLocalOrderConfirmed(SmHotelOrderEntity order) { + if (order == null || StrUtil.isBlank(order.getPartnerCode())) { + return; + } + pushOperationOrder(order.getPartnerCode(), order, OP_CONFIRM); + } + + @Override + public void notifyLocalOrderRejected(SmHotelOrderEntity order) { + if (order == null || StrUtil.isBlank(order.getPartnerCode())) { + return; + } + pushOperationOrder(order.getPartnerCode(), order, OP_REJECT); + } + private String resolvePartnerByNotifyPass(String pass) { if (StrUtil.isBlank(pass)) { return null; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java index e10998f3..7f3fea6e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java @@ -874,6 +874,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { Map pc = new LinkedHashMap<>(); pc.put("date", d.toString()); if (row == null) { + // 圣诺拒 cost=0(价格格式错误);无日历也按关房占位价 1 pc.put("cost", 1); pc.put("amount", 0); pc.put("status", 0); @@ -883,9 +884,11 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) { price = BigDecimal.ONE; } + // 整数价:四舍五入后仍可能为 0(如 0.4),圣诺要求 cost≥1 + int costInt = Math.max(1, price.setScale(0, java.math.RoundingMode.HALF_UP).intValue()); int amount = row.getRemainStock() == null ? 0 : Math.max(0, row.getRemainStock()); int open = Integer.valueOf(1).equals(row.getDailyStatus()) && amount > 0 ? 1 : 0; - pc.put("cost", price.setScale(0, java.math.RoundingMode.HALF_UP).intValue()); + pc.put("cost", costInt); pc.put("amount", open == 1 ? Math.max(1, amount) : amount); pc.put("status", open); } @@ -901,8 +904,20 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { Map body = new LinkedHashMap<>(shengnuoAdapter.signedBase(ds)); body.put("id", nsHotelId); body.put("priceInfo", priceInfo); - shengnuoAdapter.dailySync(partnerCode, ds, hotelId, String.valueOf(nsHotelId), body); - chunks++; + try { + shengnuoAdapter.dailySync(partnerCode, ds, hotelId, String.valueOf(nsHotelId), body); + chunks++; + } + catch (Exception ex) { + String msg = StrUtil.blankToDefault(ex.getMessage(), ""); + // 超窗日期对方统一回「价格日期格式错误」:已成功段保留,后续段不再推 + if (msg.contains("价格日期") || msg.contains("日期格式")) { + log.warn("|下游推送[圣诺]|价态分片超窗已跳过后续|hotelId={}|chunk={}~{}|err={}|", hotelId, + cursor, chunkEnd, msg); + break; + } + throw ex; + } } cursor = chunkEnd.plusDays(1); } @@ -1263,6 +1278,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { private LocalDate[] resolveDownstreamDailyWindow(String partnerCode) { HotelDownstreamProperties dsProps = channelConfigService.resolveDownstreamRuntime(partnerCode); int dailyDays = dsProps == null ? 90 : Math.max(1, dsProps.getDailyDays()); + // 圣诺实测:超过「今天起 15 天」会报「价格日期格式错误」(不是字符串格式问题) + if (dsProps != null && SmHotelDownstreamProtocol.isShengnuo(dsProps.getProtocol())) { + dailyDays = Math.min(dailyDays, SHENGNUO_PRICE_CHUNK_DAYS); + } LocalDate dailyFrom = LocalDate.now(); LocalDate dailyToExclusiveCap = dailyFrom.plusMonths(3); LocalDate dailyTo = dailyFrom.plusDays(dailyDays - 1L); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java index 65de228c..2d24c2de 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java @@ -8,6 +8,7 @@ import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties; import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource; import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol; import com.bsd.pjcloud.supermarket.constant.SmHotelInventoryStatus; @@ -24,9 +25,11 @@ import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService; import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService; import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService; import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAutoPushService; +import com.bsd.pjcloud.supermarket.service.SmHotelHuidingOrderService; import com.bsd.pjcloud.supermarket.service.SmHotelOrderService; import com.bsd.pjcloud.supermarket.service.SmHotelOrderUpstreamLogService; import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService; +import com.bsd.pjcloud.supermarket.service.SmHotelShengnuoOrderService; import com.bsd.pjcloud.supermarket.vo.SmHotelOrderDetailVo; import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo; import com.bsd.pjcloud.admin.api.entity.SysDept; @@ -123,8 +126,18 @@ public class SmHotelOrderServiceImpl extends ServiceImpl pageOrders(Page page, SmHotelOrderEntity query) { + List hotelIdFilter = resolveOrderHotelIdFilter(query); + if (hotelIdFilter != null && hotelIdFilter.isEmpty()) { + return page; + } Page result = baseMapper.selectPage(page, Wrappers.lambdaQuery() .like(StrUtil.isNotBlank(query.getChannelNo()), SmHotelOrderEntity::getChannelNo, query.getChannelNo()) .like(StrUtil.isNotBlank(query.getPlatformNo()), SmHotelOrderEntity::getPlatformNo, @@ -144,12 +157,38 @@ public class SmHotelOrderServiceImpl extends ServiceImpl resolveOrderHotelIdFilter(SmHotelOrderEntity query) { + boolean scope = query != null && Boolean.TRUE.equals(query.getScopeByHotel()); + String ds = query == null ? null : SmHotelDataSource.normalize(query.getHotelDataSource()); + boolean filterDs = StrUtil.isNotBlank(ds); + if (!scope && !filterDs) { + return null; + } + var wrapper = Wrappers.lambdaQuery().select(SmHotelEntity::getId); + if (filterDs) { + wrapper.eq(SmHotelEntity::getSheetName, ds); + } + List hotels = scope + ? hotelMapper.selectListByScope(wrapper, DataScope.of()) + : hotelMapper.selectList(wrapper); + if (CollUtil.isEmpty(hotels)) { + return List.of(); + } + return hotels.stream().map(SmHotelEntity::getId).filter(Objects::nonNull).distinct().toList(); + } + @Override public List> listOrderSourceOptions() { List> options = new ArrayList<>(); @@ -781,9 +820,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl local = finishLocalOrder(order, product); + R local = parkLocalPendingConfirm(order, product); notifyOpenOrderCallback(order); return local; } @@ -863,7 +902,10 @@ public class SmHotelOrderServiceImpl extends ServiceImpl parked = parkLocalPendingConfirm(order, product); + return parked != null && parked.getCode() == 0 ? R.ok(Boolean.TRUE) : R.failed( + parked == null ? "本地落单失败" : StrUtil.blankToDefault(parked.getMsg(), "本地落单失败")); } if (product == null) { order.setOrderStatus("NEED_MANUAL"); @@ -1173,7 +1215,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl local = finishLocalOrder(order, product); + R local = parkLocalPendingConfirm(order, product); if (local.getData() != null) { local.getData().set("hotelName", hotel.getHotelName()); local.getData().set("productName", product.getProductName()); @@ -1617,15 +1659,16 @@ public class SmHotelOrderServiceImpl extends ServiceImpl local = finishLocalOrder(order, product); + // 本地数据源:支付成功后占库存并待人工确认,不自动订成 + R local = parkLocalPendingConfirm(order, product); order = baseMapper.selectById(orderId); if (order == null) { return false; } order.setPayStatus(PAY_PAID); baseMapper.updateById(order); - log.info("|酒店支付成功|本地履约完成|orderId={}|ok={}|", orderId, local != null && local.isOk()); + notifyOpenOrderCallback(order); + log.info("|酒店支付成功|本地待确认|orderId={}|ok={}|", orderId, local != null && local.isOk()); return local != null && local.isOk(); } @@ -1850,41 +1893,208 @@ public class SmHotelOrderServiceImpl extends ServiceImpl finishLocalOrder(SmHotelOrderEntity order, SmHotelProductEntity product) { + private R parkLocalPendingConfirm(SmHotelOrderEntity order, SmHotelProductEntity product) { if (product == null || product.getId() == null) { throw new IllegalArgumentException("本地下单需要有效产品"); } - if (order.getCheckInDate() == null || order.getCheckOutDate() == null - || !order.getCheckInDate().isBefore(order.getCheckOutDate())) { + if (!SmHotelStayDates.isValid(order.getCheckInDate(), order.getCheckOutDate())) { throw new IllegalArgumentException("入住/离店日期无效"); } + // 已人工确认过:幂等返回 + if ("UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus())) { + return R.ok(buildOut(order), "本地订单已确认"); + } int rooms = order.getCount() == null || order.getCount() < 1 ? 1 : order.getCount(); - // App 待支付创单已预占:支付成功只确认,避免 sold 双扣 if (!SmHotelInventoryStatus.isApplied(order.getInventoryStatus())) { String deductMsg = deductLocalInventory(product.getId(), order.getCheckInDate(), order.getCheckOutDate(), rooms); if (deductMsg != null) { - // 抛错回滚本事务(含订单 insert 与已 CAS 的 sold),避免半成功 throw new IllegalArgumentException(deductMsg); } order.setInventoryStatus(SmHotelInventoryStatus.APPLIED); } order.setPlatformNo("LOCAL-" + order.getId()); - order.setOrderStatus("UPSTREAM_OK"); - order.setBookStatus(ShangyouBookStatus.R); - order.setCompensateStatus(COMP_DONE); - order.setNextCompensateTime(null); - order.setLastCompensateMsg("本地日历库存已确认"); - // 按日历卖价重算订单金额(有则覆盖) BigDecimal sellSum = sumLocalSellAmount(product.getId(), order.getCheckInDate(), order.getCheckOutDate(), rooms); if (sellSum != null) { order.setSellAmount(sellSum); } + // 环启协议无待确认码:占库存后直接确认,避免下游卡在无状态可回写 + if (isHuanqiPartnerOrder(order)) { + order.setOrderStatus("UPSTREAM_OK"); + order.setBookStatus(ShangyouBookStatus.R); + order.setCompensateStatus(COMP_DONE); + order.setNextCompensateTime(null); + order.setLastCompensateMsg("环启本地单自动确认(对方无待确认状态)"); + order.setFailMsg(null); + baseMapper.updateById(order); + return R.ok(buildOut(order), "本地下单成功"); + } + order.setOrderStatus("PENDING_CONFIRM"); + order.setBookStatus(ShangyouBookStatus.P); + order.setCompensateStatus(COMP_NONE); + order.setNextCompensateTime(null); + order.setLastCompensateMsg("本地库存已占用,待人工确认"); baseMapper.updateById(order); - return R.ok(buildOut(order), "本地下单成功"); + return R.ok(buildOut(order), "已下单,待确认"); + } + + /** 环启伙伴订单:对方无待确认/新订拒绝状态码,本地源需即时确认 */ + private boolean isHuanqiPartnerOrder(SmHotelOrderEntity order) { + if (order == null || StrUtil.isBlank(order.getPartnerCode())) { + return false; + } + try { + HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(order.getPartnerCode()); + return props != null && SmHotelDownstreamProtocol.isHuanqi(props.getProtocol()); + } + catch (Exception e) { + log.warn("|本地落单|解析伙伴协议失败|partner={}|{}", order.getPartnerCode(), e.getMessage()); + return false; + } + } + + /** + * 本地人工确认:PENDING_CONFIRM → UPSTREAM_OK。 + */ + private R finishLocalOrderConfirmed(SmHotelOrderEntity order) { + if (order == null) { + return R.failed("订单不存在"); + } + order.setPlatformNo(StrUtil.blankToDefault(order.getPlatformNo(), "LOCAL-" + order.getId())); + order.setOrderStatus("UPSTREAM_OK"); + order.setBookStatus(ShangyouBookStatus.R); + order.setCompensateStatus(COMP_DONE); + order.setNextCompensateTime(null); + order.setLastCompensateMsg("本地人工确认接单"); + order.setFailMsg(null); + baseMapper.updateById(order); + return R.ok(buildOut(order), "本地确认成功"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R confirmLocalByAdmin(Long id, String reason) { + SmHotelOrderEntity order = baseMapper.selectById(id); + if (order == null) { + return R.failed("订单不存在"); + } + String scopeErr = assertOrderHotelInScope(order); + if (scopeErr != null) { + return R.failed(scopeErr); + } + if ("UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus())) { + return R.ok(true, "订单已确认"); + } + if (!"PENDING_CONFIRM".equals(order.getOrderStatus())) { + return R.failed("仅待确认订单可接单,当前状态=" + order.getOrderStatus()); + } + SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId()); + if (hotel == null || !SmHotelDataSource.isLocal(hotel.getSheetName())) { + return R.failed("仅本地数据来源酒店订单可人工确认"); + } + R done = finishLocalOrderConfirmed(order); + if (done == null || done.getCode() != 0) { + return R.failed(done == null ? "确认失败" : StrUtil.blankToDefault(done.getMsg(), "确认失败")); + } + order = baseMapper.selectById(id); + if (StrUtil.isNotBlank(reason) && order != null) { + order.setLastCompensateMsg(StrUtil.sub("本地人工确认接单:" + reason, 0, 500)); + baseMapper.updateById(order); + } + notifyDownstreamAfterLocalDecision(order, true); + notifyOpenOrderCallback(order); + return R.ok(true, "已确认接单"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R rejectLocalByAdmin(Long id, String reason) { + SmHotelOrderEntity order = baseMapper.selectById(id); + if (order == null) { + return R.failed("订单不存在"); + } + String scopeErr = assertOrderHotelInScope(order); + if (scopeErr != null) { + return R.failed(scopeErr); + } + if ("CANCELLED".equals(order.getOrderStatus()) || PAY_REFUNDED.equals(order.getPayStatus())) { + return R.ok(true, "订单已关闭"); + } + if (!"PENDING_CONFIRM".equals(order.getOrderStatus())) { + return R.failed("仅待确认订单可拒绝,当前状态=" + order.getOrderStatus()); + } + SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId()); + if (hotel == null || !SmHotelDataSource.isLocal(hotel.getSheetName())) { + return R.failed("仅本地数据来源酒店订单可人工拒绝"); + } + String rejectReason = StrUtil.blankToDefault(reason, "人工拒绝接单"); + // 先通知下游拒绝(订单仍为待确认),再关单/退款 + notifyDownstreamAfterLocalDecision(order, false); + if (PAY_PAID.equals(order.getPayStatus())) { + R refundR = refundByAdmin(id, rejectReason); + if (refundR == null || !refundR.isOk()) { + return R.failed(refundR == null ? "拒绝失败:退款异常" : refundR.getMsg()); + } + return R.ok(true, "已拒绝并退款"); + } + R cancelR = cancelByAdmin(id, rejectReason); + if (cancelR == null || !cancelR.isOk()) { + return R.failed(cancelR == null ? "拒绝失败" : cancelR.getMsg()); + } + return R.ok(true, "已拒绝并取消"); + } + + /** 确认/拒绝时校验订单酒店在当前用户 DataScope 内(无酒店 ID 则拒绝) */ + private String assertOrderHotelInScope(SmHotelOrderEntity order) { + if (order == null || order.getHotelId() == null) { + return "订单缺少酒店信息"; + } + List hotels = hotelMapper.selectListByScope( + Wrappers.lambdaQuery() + .select(SmHotelEntity::getId) + .eq(SmHotelEntity::getId, order.getHotelId()), + DataScope.of()); + if (CollUtil.isEmpty(hotels)) { + return "无权操作该酒店订单"; + } + return null; + } + + /** 按伙伴协议回写下游:确认/拒绝 */ + private void notifyDownstreamAfterLocalDecision(SmHotelOrderEntity order, boolean confirmed) { + if (order == null || StrUtil.isBlank(order.getPartnerCode())) { + return; + } + try { + HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(order.getPartnerCode()); + String protocol = props == null ? null : props.getProtocol(); + if (SmHotelDownstreamProtocol.isHuiding(protocol)) { + if (confirmed) { + huidingOrderService.notifyLocalOrderConfirmed(order); + } + else { + huidingOrderService.notifyLocalOrderRejected(order); + } + return; + } + if (SmHotelDownstreamProtocol.isShengnuo(protocol)) { + if (confirmed) { + shengnuoOrderService.notifyLocalOrderConfirmed(order); + } + else { + shengnuoOrderService.notifyLocalOrderRejected(order); + } + } + } + catch (Exception e) { + log.warn("|本地订单人工确认|下游通知失败|orderId={}|confirmed={}|{}", order.getId(), confirmed, + e.getMessage()); + } } /** diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java index 084ded49..d636f081 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java @@ -149,18 +149,35 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ if (latest == null) { return fail("下单后订单丢失"); } - if (book == null || book.getCode() != 0 || !isBookConfirmed(latest)) { + if (book == null || book.getCode() != 0) { String failMsg = latest.getFailMsg(); if (StrUtil.isBlank(failMsg)) { failMsg = book == null ? "上游下单失败" : StrUtil.blankToDefault(book.getMsg(), "上游下单失败"); } + if (!"UPSTREAM_FAIL".equals(latest.getOrderStatus()) && !"NEED_MANUAL".equals(latest.getOrderStatus()) + && !"CANCELLED".equals(latest.getOrderStatus()) + && !"PENDING_CONFIRM".equals(latest.getOrderStatus())) { + latest.setOrderStatus("UPSTREAM_FAIL"); + latest.setFailMsg(StrUtil.maxLength(failMsg, 500)); + orderMapper.updateById(latest); + } + // 无法接单:不主动回写拒绝态(文档无「新订拒绝」码);保持待确认由对方超时处理 + pushOrderChange(partnerCode, orderId, ST_PENDING); + return fail(failMsg); + } + // 本地酒店:待人工确认,回写待确认(1),成功返回 + if (isPendingConfirm(latest)) { + pushOrderChange(partnerCode, orderId, ST_PENDING); + return ok("下单成功,待确认", latest.getId()); + } + if (!isBookConfirmed(latest)) { + String failMsg = StrUtil.blankToDefault(latest.getFailMsg(), "上游下单未确认"); if (!"UPSTREAM_FAIL".equals(latest.getOrderStatus()) && !"NEED_MANUAL".equals(latest.getOrderStatus()) && !"CANCELLED".equals(latest.getOrderStatus())) { latest.setOrderStatus("UPSTREAM_FAIL"); latest.setFailMsg(StrUtil.maxLength(failMsg, 500)); orderMapper.updateById(latest); } - // 无法接单:不主动回写拒绝态(文档无「新订拒绝」码);保持待确认由对方超时处理 pushOrderChange(partnerCode, orderId, ST_PENDING); return fail(failMsg); } @@ -302,7 +319,7 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ } private static boolean isBookConfirmed(SmHotelOrderEntity order) { - if (order == null) { + if (order == null || isPendingConfirm(order)) { return false; } if ("UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus())) { @@ -314,6 +331,26 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ && !"CANCELLED".equals(order.getOrderStatus()); } + private static boolean isPendingConfirm(SmHotelOrderEntity order) { + return order != null && "PENDING_CONFIRM".equals(order.getOrderStatus()); + } + + @Override + public void notifyLocalOrderConfirmed(SmHotelOrderEntity order) { + if (order == null || StrUtil.isBlank(order.getPartnerCode()) || StrUtil.isBlank(order.getChannelNo())) { + return; + } + pushOrderChange(order.getPartnerCode(), order.getChannelNo(), ST_ACCEPTED); + } + + @Override + public void notifyLocalOrderRejected(SmHotelOrderEntity order) { + if (order == null || StrUtil.isBlank(order.getPartnerCode()) || StrUtil.isBlank(order.getChannelNo())) { + return; + } + pushOrderChange(order.getPartnerCode(), order.getChannelNo(), ST_CANCELLED); + } + private static Map ok(String msg, Long localId) { Map m = new LinkedHashMap<>(6); m.put("code", 0);