酒店修改待确认

This commit is contained in:
zjf committed 2026-10-27 19:30:31 +08:00
1 parent 7e81f3655a
commit c02a897876
9 files changed
+397 -35

No files matched your search

@@ -159,11 +159,16 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
@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")
@@ -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<SmHotelOrderEntity>> page(@ParameterObject Page<SmHotelOrderEntity> 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<java.util.List<java.util.Map<String, Object>>> 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<SmHotelOrderDetailVo> 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<Boolean> confirm(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> 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<Boolean> reject(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> body) {
String reason = body == null ? null : String.valueOf(body.getOrDefault("reason", "人工拒绝接单"));
return orderService.rejectLocalByAdmin(id, reason);
}
@SysLog("后台退款酒店订单")
@Operation(summary = "后台退款:已支付订单原路退款并关闭预订")
@PostMapping("/refund/{id}")
@@ -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<JSONObject> queryRemote(String partnerCode, String ordercode, String childcode);
/** 本地酒店人工确认后回写慧订 operationOrder=1 */
void notifyLocalOrderConfirmed(SmHotelOrderEntity order);
/** 本地酒店人工拒绝后回写慧订 operationOrder=2 */
void notifyLocalOrderRejected(SmHotelOrderEntity order);
}
@@ -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<Boolean> cancelByAdmin(Long id, String reason);
/**
* 本地数据源订单人工确认接单:PENDING_CONFIRM → UPSTREAM_OK,并通知下游。
*/
R<Boolean> confirmLocalByAdmin(Long id, String reason);
/**
* 本地数据源订单人工拒绝:取消并释库存;已支付则原路退款;通知下游拒绝。
*/
R<Boolean> rejectLocalByAdmin(Long id, String reason);
/** Worker:扫描到期补偿任务 */
int processDueCompensate(int limit);
@@ -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<String, Object> orderConfirmTimeOut(String partnerCode, JSONObject body);
/** 本地酒店人工确认后 orderChange=已接单(2) */
void notifyLocalOrderConfirmed(SmHotelOrderEntity order);
/** 本地酒店人工拒绝后 orderChange=已取消(5) */
void notifyLocalOrderRejected(SmHotelOrderEntity order);
}
@@ -208,17 +208,36 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
order.setHuanqiPayload(data.toString());
orderMapper.insert(order);
// 同步订上游:当场返回成败,不走「先待确认再 operationOrder」中间态
// 本地酒店:占库存后 PENDING_CONFIRM,返回待确认,人工确认后再 operationOrder=1
// 上游酒店:同步订上游,成功当场确认
R<Boolean> 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<Boolean> 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;
@@ -874,6 +874,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
Map<String, Object> 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<String, Object> 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);
@@ -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<SmHotelOrderMapper, SmH
/** 本地占/放库存后防抖推下游(环启等),推剩余可售而非裸配额 */
private final @Lazy SmHotelDownstreamAutoPushService downstreamAutoPushService;
/** 本地人工确认后回写慧订(Lazy 避免与订单入站循环依赖) */
private final @Lazy SmHotelHuidingOrderService huidingOrderService;
/** 本地人工确认后回写圣诺 */
private final @Lazy SmHotelShengnuoOrderService shengnuoOrderService;
@Override
public Page<SmHotelOrderEntity> pageOrders(Page<SmHotelOrderEntity> page, SmHotelOrderEntity query) {
List<Long> hotelIdFilter = resolveOrderHotelIdFilter(query);
if (hotelIdFilter != null && hotelIdFilter.isEmpty()) {
return page;
}
Page<SmHotelOrderEntity> result = baseMapper.selectPage(page, Wrappers.<SmHotelOrderEntity>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<SmHotelOrderMapper, SmH
.like(StrUtil.isNotBlank(query.getContactPhone()), SmHotelOrderEntity::getContactPhone,
query.getContactPhone())
.eq(StrUtil.isNotBlank(query.getCreateBy()), SmHotelOrderEntity::getCreateBy, query.getCreateBy())
.in(CollUtil.isNotEmpty(hotelIdFilter), SmHotelOrderEntity::getHotelId, hotelIdFilter)
.orderByDesc(SmHotelOrderEntity::getCreateTime));
// 后台列表补齐酒店名与来源,便于识别 App 下单
fillOrderListDisplay(result.getRecords());
return result;
}
/**
* 订单列表酒店过滤:管家 scopeByHotel 按 DataScope;hotelDataSource 按来源收窄。
*
* @return null=不过滤;empty=无可见酒店直接空页;非空=in 过滤
*/
private List<Long> 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.<SmHotelEntity>lambdaQuery().select(SmHotelEntity::getId);
if (filterDs) {
wrapper.eq(SmHotelEntity::getSheetName, ds);
}
List<SmHotelEntity> 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<Map<String, Object>> listOrderSourceOptions() {
List<Map<String, Object>> options = new ArrayList<>();
@@ -781,9 +820,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
}
baseMapper.insert(order);
// 本地酒店:扣本地日历库存,不走上游
// 本地酒店:占库存并待人工确认,不走上游
if (localHotel) {
R<JSONObject> local = finishLocalOrder(order, product);
R<JSONObject> local = parkLocalPendingConfirm(order, product);
notifyOpenOrderCallback(order);
return local;
}
@@ -863,7 +902,10 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
if (product == null) {
return R.failed("本地酒店缺产品");
}
return finishLocalOrder(order, product).getCode() == 0 ? R.ok(Boolean.TRUE) : R.failed("本地落单失败");
// 本地源:只占库存进入待确认,等 PC/管家人工确认后再订成
R<JSONObject> 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<SmHotelOrderMapper, SmH
baseMapper.insert(order);
if (localHotel) {
R<JSONObject> local = finishLocalOrder(order, product);
R<JSONObject> 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<SmHotelOrderMapper, SmH
log.error("|酒店支付成功|本地履约缺产品|orderId={}|", orderId);
return false;
}
// 本地数据源:只确认本地库存,不回调上游
R<JSONObject> local = finishLocalOrder(order, product);
// 本地数据源:支付成功后占库存并待人工确认,不自动订成
R<JSONObject> 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<SmHotelOrderMapper, SmH
}
/**
* 本地酒店确认:若待支付阶段已预占库存则不再二次扣减,只落确认状态。
* 本地酒店落单:占库存;默认待人工确认。
* 环启无「待确认」状态码,环启伙伴订单占库存后直接订成。
* App 待支付已预占时不二次扣减,避免 sold 双扣。
*/
private R<JSONObject> finishLocalOrder(SmHotelOrderEntity order, SmHotelProductEntity product) {
private R<JSONObject> 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<JSONObject> 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<Boolean> 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<JSONObject> 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<Boolean> 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<Boolean> refundR = refundByAdmin(id, rejectReason);
if (refundR == null || !refundR.isOk()) {
return R.failed(refundR == null ? "拒绝失败:退款异常" : refundR.getMsg());
}
return R.ok(true, "已拒绝并退款");
}
R<Boolean> 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<SmHotelEntity> hotels = hotelMapper.selectListByScope(
Wrappers.<SmHotelEntity>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());
}
}
/**
@@ -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<String, Object> ok(String msg, Long localId) {
Map<String, Object> m = new LinkedHashMap<>(6);
m.put("code", 0);