酒店修改待确认
This commit is contained in:
1 parent
7e81f3655a
commit
c02a897876
9 files changed
+397
-35
No files matched your search
+6
-1
@@ -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")
|
||||
|
||||
+21
-3
@@ -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}")
|
||||
|
||||
+7
@@ -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);
|
||||
}
|
||||
+11
-1
@@ -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);
|
||||
|
||||
|
||||
+7
@@ -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);
|
||||
}
|
||||
+53
-4
@@ -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;
|
||||
|
||||
+22
-3
@@ -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);
|
||||
|
||||
+230
-20
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+40
-3
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user