小如酒店,优化修复部门界面问题
This commit is contained in:
1 parent
72fd5a4358
commit
5b88ce844c
55 files changed
+4723
-6
No files matched your search
+20
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.module.product.api.hotel;
|
||||
|
||||
/**
|
||||
* 酒店订单支付回调桥接(与商城共用 mall 支付应用)。
|
||||
* 商户单号可能与 trade_order.id 撞号,必须以支付单反查酒店单。
|
||||
*/
|
||||
public interface HotelZaxOrderApi {
|
||||
|
||||
/**
|
||||
* 支付成功通知:优先按 payOrderId 匹配酒店单。
|
||||
*
|
||||
* @return true=已按酒店单处理;false=不是酒店单,应继续走商城订单
|
||||
*/
|
||||
boolean handlePayNotify(String merchantOrderId, Long payOrderId);
|
||||
|
||||
/**
|
||||
* @return true=已按酒店单处理;false=不是酒店单
|
||||
*/
|
||||
boolean updatePaidIfMatch(Long merchantOrderId, Long payOrderId);
|
||||
}
|
||||
+8
@@ -75,4 +75,12 @@ public interface ErrorCodeConstants {
|
||||
// ========== 贩卖机测试日志 1-008-013-000 ==========
|
||||
ErrorCode DEVICE_LOG_NOT_EXISTS = new ErrorCode(1_008_013_000, "贩卖机测试日志不存在");
|
||||
|
||||
// ========== 资安心酒店本地库 1-008-020-000 ==========
|
||||
ErrorCode HOTEL_ZAX_NOT_EXISTS = new ErrorCode(1_008_020_000, "本地酒店不存在");
|
||||
ErrorCode HOTEL_ZAX_ROOM_NOT_EXISTS = new ErrorCode(1_008_020_001, "本地酒店房型不存在");
|
||||
ErrorCode HOTEL_ZAX_PRODUCT_NOT_EXISTS = new ErrorCode(1_008_020_002, "本地酒店产品不存在");
|
||||
ErrorCode HOTEL_ZAX_DAILY_NOT_EXISTS = new ErrorCode(1_008_020_003, "本地酒店日历不存在");
|
||||
ErrorCode HOTEL_ZAX_ORDER_NOT_EXISTS = new ErrorCode(1_008_020_004, "本地酒店订单不存在");
|
||||
ErrorCode HOTEL_ZAX_ORDER_CANCEL_FAIL = new ErrorCode(1_008_020_005, "酒店订单取消失败:{}");
|
||||
|
||||
}
|
||||
@@ -28,6 +28,11 @@
|
||||
<artifactId>yudao-module-member-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-module-pay-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package cn.iocoder.yudao.module.product.api.hotel;
|
||||
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxTradeService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
public class HotelZaxOrderApiImpl implements HotelZaxOrderApi {
|
||||
|
||||
@Resource
|
||||
private HotelZaxTradeService hotelZaxTradeService;
|
||||
|
||||
@Override
|
||||
public boolean handlePayNotify(String merchantOrderId, Long payOrderId) {
|
||||
return hotelZaxTradeService.handlePayNotify(merchantOrderId, payOrderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updatePaidIfMatch(Long merchantOrderId, Long payOrderId) {
|
||||
return hotelZaxTradeService.updatePaidIfMatch(merchantOrderId, payOrderId);
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyBatchReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailySaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxOrderPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxOrderRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxProductRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxProductSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRoomRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRoomSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxStatusReqVO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxSyncCursorDO;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxSyncCursorMapper;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxAdminService;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxSyncService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 资安心酒店本地库")
|
||||
@RestController
|
||||
@RequestMapping("/product/hotel-zax")
|
||||
@Validated
|
||||
public class HotelZaxController {
|
||||
|
||||
@Resource
|
||||
private HotelZaxMapper hotelZaxMapper;
|
||||
|
||||
@Resource
|
||||
private HotelZaxSyncCursorMapper cursorMapper;
|
||||
|
||||
@Resource
|
||||
private HotelZaxSyncService hotelZaxSyncService;
|
||||
|
||||
@Resource
|
||||
private HotelZaxAdminService hotelZaxAdminService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "本地酒店分页")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:query')")
|
||||
public CommonResult<PageResult<HotelZaxRespVO>> page(@Valid HotelZaxPageReqVO req) {
|
||||
return success(hotelZaxAdminService.page(req));
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "本地酒店详情(含房型/产品)")
|
||||
@Parameter(name = "id", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:query')")
|
||||
public CommonResult<HotelZaxRespVO> get(@RequestParam("id") Long id) {
|
||||
return success(hotelZaxAdminService.getDetail(id));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "编辑本地酒店基础信息")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<Boolean> update(@Valid @RequestBody HotelZaxSaveReqVO req) {
|
||||
hotelZaxAdminService.update(req);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/update-status")
|
||||
@Operation(summary = "本地上下架")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<Boolean> updateStatus(@Valid @RequestBody HotelZaxStatusReqVO req) {
|
||||
hotelZaxAdminService.updateStatus(req);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/room/save")
|
||||
@Operation(summary = "保存房型")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<HotelZaxRoomRespVO> saveRoom(@Valid @RequestBody HotelZaxRoomSaveReqVO req) {
|
||||
return success(hotelZaxAdminService.saveRoom(req));
|
||||
}
|
||||
|
||||
@DeleteMapping("/room/delete")
|
||||
@Operation(summary = "删除房型")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<Boolean> removeRoom(@RequestParam("hotelId") Long hotelId,
|
||||
@RequestParam("roomId") Long roomId) {
|
||||
hotelZaxAdminService.removeRoom(hotelId, roomId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/product/save")
|
||||
@Operation(summary = "保存产品")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<HotelZaxProductRespVO> saveProduct(@Valid @RequestBody HotelZaxProductSaveReqVO req) {
|
||||
return success(hotelZaxAdminService.saveProduct(req));
|
||||
}
|
||||
|
||||
@DeleteMapping("/product/delete")
|
||||
@Operation(summary = "删除产品")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<Boolean> removeProduct(@RequestParam("hotelId") Long hotelId,
|
||||
@RequestParam("productId") Long productId) {
|
||||
hotelZaxAdminService.removeProduct(hotelId, productId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/daily/page")
|
||||
@Operation(summary = "本地酒店日历分页")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:query')")
|
||||
public CommonResult<PageResult<HotelZaxDailyRespVO>> pageDaily(@Valid HotelZaxDailyPageReqVO req) {
|
||||
return success(hotelZaxAdminService.pageDaily(req));
|
||||
}
|
||||
|
||||
@PostMapping("/daily/save")
|
||||
@Operation(summary = "保存日历")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<HotelZaxDailyRespVO> saveDaily(@Valid @RequestBody HotelZaxDailySaveReqVO req) {
|
||||
return success(hotelZaxAdminService.saveDaily(req));
|
||||
}
|
||||
|
||||
@PostMapping("/daily/batch")
|
||||
@Operation(summary = "批量生成/调价日历 FILL/ADJUST")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<Map<String, Object>> batchDaily(@Valid @RequestBody HotelZaxDailyBatchReqVO req) {
|
||||
return success(hotelZaxAdminService.batchDaily(req));
|
||||
}
|
||||
|
||||
@DeleteMapping("/daily/delete")
|
||||
@Operation(summary = "删除日历")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:update')")
|
||||
public CommonResult<Boolean> removeDaily(@RequestParam("hotelId") Long hotelId,
|
||||
@RequestParam("dailyId") Long dailyId) {
|
||||
hotelZaxAdminService.removeDaily(hotelId, dailyId);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/daily/sync")
|
||||
@Operation(summary = "从资安心拉取该酒店日历并落本地")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:sync')")
|
||||
public CommonResult<Map<String, Object>> syncDaily(@RequestParam("hotelId") Long hotelId) {
|
||||
return success(hotelZaxSyncService.syncDailyByHotelId(hotelId));
|
||||
}
|
||||
|
||||
@GetMapping("/sync-cursor")
|
||||
@Operation(summary = "同步水位")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:query')")
|
||||
public CommonResult<Map<String, Object>> syncCursor() {
|
||||
HotelZaxSyncCursorDO c = cursorMapper.selectOne(new LambdaQueryWrapperX<HotelZaxSyncCursorDO>()
|
||||
.eq(HotelZaxSyncCursorDO::getCursorKey, "default")
|
||||
.last("limit 1"));
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
if (c != null) {
|
||||
m.put("updatedSince", c.getUpdatedSince());
|
||||
m.put("lastSyncAt", c.getLastSyncAt());
|
||||
m.put("lastMsg", c.getLastMsg());
|
||||
}
|
||||
long total = hotelZaxMapper.selectCount();
|
||||
long active = hotelZaxMapper.selectCount(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getStatus, "ACTIVE"));
|
||||
m.put("total", total);
|
||||
m.put("active", active);
|
||||
return success(m);
|
||||
}
|
||||
|
||||
@GetMapping("/order/page")
|
||||
@Operation(summary = "本地酒店订单分页")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax-order:query')")
|
||||
public CommonResult<PageResult<HotelZaxOrderRespVO>> pageOrders(@Valid HotelZaxOrderPageReqVO req) {
|
||||
return success(hotelZaxAdminService.pageOrders(req));
|
||||
}
|
||||
|
||||
@GetMapping("/order/get")
|
||||
@Operation(summary = "本地酒店订单详情")
|
||||
@Parameter(name = "id", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax-order:query')")
|
||||
public CommonResult<HotelZaxOrderRespVO> getOrder(@RequestParam("id") Long id) {
|
||||
return success(hotelZaxAdminService.getOrder(id));
|
||||
}
|
||||
|
||||
@PostMapping("/order/cancel")
|
||||
@Operation(summary = "后台取消酒店订单(已支付则退款,并通知资安心)")
|
||||
@Parameter(name = "id", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax-order:query')")
|
||||
public CommonResult<Boolean> cancelOrder(@RequestParam("id") Long id,
|
||||
@RequestParam(value = "reason", required = false) String reason) {
|
||||
hotelZaxAdminService.cancelOrder(id, reason);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/sync")
|
||||
@Operation(summary = "从资安心开放平台同步到本地(默认全量,适配授权范围变更)")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:sync')")
|
||||
public CommonResult<Map<String, Object>> sync(
|
||||
@RequestParam(value = "full", required = false, defaultValue = "true") Boolean full) {
|
||||
return success(hotelZaxSyncService.syncOnce(Boolean.TRUE.equals(full)));
|
||||
}
|
||||
|
||||
@PostMapping("/reset-cursor")
|
||||
@Operation(summary = "重置同步水位(下次全量增量起点)")
|
||||
@PreAuthorize("@ss.hasPermission('product:hotel-zax:sync')")
|
||||
public CommonResult<Boolean> resetCursor() {
|
||||
HotelZaxSyncCursorDO c = cursorMapper.selectOne(new LambdaQueryWrapperX<HotelZaxSyncCursorDO>()
|
||||
.eq(HotelZaxSyncCursorDO::getCursorKey, "default")
|
||||
.last("limit 1"));
|
||||
if (c != null) {
|
||||
c.setUpdatedSince(null);
|
||||
c.setLastMsg(StrUtil.blankToDefault(c.getLastMsg(), "") + " | reset");
|
||||
cursorMapper.updateById(c);
|
||||
}
|
||||
return success(true);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店日历批量生成/调价")
|
||||
@Data
|
||||
public class HotelZaxDailyBatchReqVO {
|
||||
|
||||
@NotNull(message = "酒店 id 不能为空")
|
||||
private Long hotelId;
|
||||
|
||||
@NotBlank(message = "mode 不能为空")
|
||||
@Schema(description = "FILL 补齐缺失 / ADJUST 改已有")
|
||||
private String mode;
|
||||
|
||||
@NotNull(message = "开始日期不能为空")
|
||||
private LocalDate startDate;
|
||||
|
||||
@NotNull(message = "结束日期不能为空")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Schema(description = "空=全部产品")
|
||||
private List<String> productCodes;
|
||||
|
||||
private BigDecimal costPrice;
|
||||
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
private Integer stock;
|
||||
|
||||
private Integer dailyStatus;
|
||||
|
||||
@Schema(description = "ADJUST 时是否同时补齐缺失日期")
|
||||
private Boolean fillMissing;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店日历分页")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class HotelZaxDailyPageReqVO extends PageParam {
|
||||
|
||||
@NotNull(message = "酒店 id 不能为空")
|
||||
private Long hotelId;
|
||||
|
||||
private String productCode;
|
||||
|
||||
@Schema(description = "入住日起 yyyy-MM-dd")
|
||||
private String stayFrom;
|
||||
|
||||
@Schema(description = "入住日止 yyyy-MM-dd")
|
||||
private String stayTo;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店日历")
|
||||
@Data
|
||||
public class HotelZaxDailyRespVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private LocalDate stayDate;
|
||||
|
||||
private BigDecimal costPrice;
|
||||
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
private Integer stock;
|
||||
|
||||
private Integer sold;
|
||||
|
||||
private Integer remainStock;
|
||||
|
||||
/** 0关 1开 */
|
||||
private Integer dailyStatus;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店日历保存")
|
||||
@Data
|
||||
public class HotelZaxDailySaveReqVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "酒店 id 不能为空")
|
||||
private Long hotelId;
|
||||
|
||||
@NotBlank(message = "产品编码不能为空")
|
||||
private String productCode;
|
||||
|
||||
@NotNull(message = "入住日期不能为空")
|
||||
private LocalDate stayDate;
|
||||
|
||||
private BigDecimal costPrice;
|
||||
|
||||
@NotNull(message = "卖价不能为空")
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
@NotNull(message = "当日配额不能为空")
|
||||
private Integer stock;
|
||||
|
||||
@NotNull(message = "房态不能为空")
|
||||
private Integer dailyStatus;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店订单分页")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class HotelZaxOrderPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "酒店名称")
|
||||
private String hotelName;
|
||||
|
||||
@Schema(description = "酒店编码")
|
||||
private String hotelCode;
|
||||
|
||||
@Schema(description = "渠道单号")
|
||||
private String channelNo;
|
||||
|
||||
@Schema(description = "供应方单号")
|
||||
private String orderNo;
|
||||
|
||||
@Schema(description = "本地状态 UNPAID/PAID/CREATED/BOOK_FAIL")
|
||||
private String orderStatus;
|
||||
|
||||
@Schema(description = "是否已支付")
|
||||
private Boolean payStatus;
|
||||
|
||||
@Schema(description = "联系人手机")
|
||||
private String contactPhone;
|
||||
|
||||
@Schema(description = "下单用户")
|
||||
private Long userId;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店订单")
|
||||
@Data
|
||||
public class HotelZaxOrderRespVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String channelNo;
|
||||
|
||||
private String orderNo;
|
||||
|
||||
private String platformNo;
|
||||
|
||||
private String orderStatus;
|
||||
|
||||
private String bookStatus;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String hotelName;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private String productName;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private LocalDate checkInDate;
|
||||
|
||||
private LocalDate checkOutDate;
|
||||
|
||||
private Integer nights;
|
||||
|
||||
private Integer roomCount;
|
||||
|
||||
private String contactName;
|
||||
|
||||
private String contactPhone;
|
||||
|
||||
private String guestJson;
|
||||
|
||||
private Long payOrderId;
|
||||
|
||||
private Boolean payStatus;
|
||||
|
||||
private LocalDateTime payTime;
|
||||
|
||||
private BigDecimal sellAmount;
|
||||
|
||||
private String failMsg;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店分页")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class HotelZaxPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "酒店名称")
|
||||
private String hotelName;
|
||||
|
||||
@Schema(description = "酒店编码")
|
||||
private String hotelCode;
|
||||
|
||||
@Schema(description = "状态 ACTIVE/OFFLINE")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "区域 adcode")
|
||||
private Long districtAdcode;
|
||||
|
||||
@Schema(description = "区域名称")
|
||||
private String district;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店产品")
|
||||
@Data
|
||||
public class HotelZaxProductRespVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String groupCode;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private String productName;
|
||||
|
||||
private String priceCode;
|
||||
|
||||
private String cancelCode;
|
||||
|
||||
private Integer mealType;
|
||||
|
||||
private Integer mealCount;
|
||||
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
private Integer stock;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店产品保存")
|
||||
@Data
|
||||
public class HotelZaxProductSaveReqVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "酒店 id 不能为空")
|
||||
private Long hotelId;
|
||||
|
||||
@NotBlank(message = "产品编码不能为空")
|
||||
private String productCode;
|
||||
|
||||
@NotBlank(message = "产品名称不能为空")
|
||||
private String productName;
|
||||
|
||||
@NotBlank(message = "所属房型不能为空")
|
||||
private String roomTypeCode;
|
||||
|
||||
private String priceCode;
|
||||
|
||||
private String cancelCode;
|
||||
|
||||
private Integer mealType;
|
||||
|
||||
private Integer mealCount;
|
||||
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
private Integer stock;
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店")
|
||||
@Data
|
||||
public class HotelZaxRespVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String hotelName;
|
||||
|
||||
private String groupCode;
|
||||
|
||||
private String coverImage;
|
||||
|
||||
private String carouselImages;
|
||||
|
||||
private String address;
|
||||
|
||||
private String district;
|
||||
|
||||
private Long districtAdcode;
|
||||
|
||||
private String bizArea;
|
||||
|
||||
private Integer starLevel;
|
||||
|
||||
private BigDecimal score;
|
||||
|
||||
private String facilityTags;
|
||||
|
||||
private BigDecimal longitude;
|
||||
|
||||
private BigDecimal latitude;
|
||||
|
||||
private String phone;
|
||||
|
||||
private BigDecimal minSellPrice;
|
||||
|
||||
private String status;
|
||||
|
||||
private String intro;
|
||||
|
||||
private LocalDateTime supplierUpdateTime;
|
||||
|
||||
private LocalDateTime syncedAt;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "房型数")
|
||||
private Integer roomCount;
|
||||
|
||||
@Schema(description = "产品数")
|
||||
private Integer productCount;
|
||||
|
||||
@Schema(description = "房型列表(详情)")
|
||||
private List<HotelZaxRoomRespVO> rooms;
|
||||
|
||||
@Schema(description = "产品列表(详情)")
|
||||
private List<HotelZaxProductRespVO> products;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店房型")
|
||||
@Data
|
||||
public class HotelZaxRoomRespVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String groupCode;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private String roomTypeName;
|
||||
|
||||
private String bedType;
|
||||
|
||||
private Integer capacity;
|
||||
|
||||
private Integer roomNumber;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店房型保存")
|
||||
@Data
|
||||
public class HotelZaxRoomSaveReqVO {
|
||||
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "酒店 id 不能为空")
|
||||
private Long hotelId;
|
||||
|
||||
@NotBlank(message = "房型编码不能为空")
|
||||
private String roomTypeCode;
|
||||
|
||||
@NotBlank(message = "房型名称不能为空")
|
||||
private String roomTypeName;
|
||||
|
||||
private String bedType;
|
||||
|
||||
private Integer capacity;
|
||||
|
||||
private Integer roomNumber;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店保存")
|
||||
@Data
|
||||
public class HotelZaxSaveReqVO {
|
||||
|
||||
@NotNull(message = "id 不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "酒店名称不能为空")
|
||||
private String hotelName;
|
||||
|
||||
private String coverImage;
|
||||
|
||||
private String carouselImages;
|
||||
|
||||
private String address;
|
||||
|
||||
private String district;
|
||||
|
||||
private Long districtAdcode;
|
||||
|
||||
private String bizArea;
|
||||
|
||||
private Integer starLevel;
|
||||
|
||||
private BigDecimal score;
|
||||
|
||||
private String facilityTags;
|
||||
|
||||
private BigDecimal longitude;
|
||||
|
||||
private BigDecimal latitude;
|
||||
|
||||
private String phone;
|
||||
|
||||
private BigDecimal minSellPrice;
|
||||
|
||||
/** ACTIVE / OFFLINE */
|
||||
private String status;
|
||||
|
||||
private String intro;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package cn.iocoder.yudao.module.product.controller.admin.hotel.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "管理后台 - 本地酒店上下架")
|
||||
@Data
|
||||
public class HotelZaxStatusReqVO {
|
||||
|
||||
@NotNull(message = "id 不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "状态不能为空")
|
||||
private String status;
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package cn.iocoder.yudao.module.product.controller.app.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenClient;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxLocalService;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxSyncService;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxTradeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.annotation.security.PermitAll;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
|
||||
/**
|
||||
* 小如酒店:列表/详情本地;交易走供应方;支付在小如微信。
|
||||
*/
|
||||
@Tag(name = "用户 APP - 酒店预订(资安心供应)")
|
||||
@RestController
|
||||
@RequestMapping("/hotel")
|
||||
@Validated
|
||||
public class AppHotelController {
|
||||
|
||||
@Resource
|
||||
private ZaxHotelOpenClient zaxHotelOpenClient;
|
||||
|
||||
@Resource
|
||||
private HotelZaxLocalService hotelZaxLocalService;
|
||||
|
||||
@Resource
|
||||
private HotelZaxSyncService hotelZaxSyncService;
|
||||
|
||||
@Resource
|
||||
private HotelZaxTradeService hotelZaxTradeService;
|
||||
|
||||
@PermitAll
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "酒店列表(优先本地)")
|
||||
public CommonResult<Object> list(@RequestBody(required = false) Map<String, Object> body) {
|
||||
if (hotelZaxLocalService.hasLocalData()) {
|
||||
return success(hotelZaxLocalService.pageList(body));
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/list", body)));
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
@PostMapping("/filter-meta")
|
||||
@Operation(summary = "筛选项(区域等,优先本地)")
|
||||
public CommonResult<Object> filterMeta(@RequestBody(required = false) Map<String, Object> body) {
|
||||
if (hotelZaxLocalService.hasLocalData()) {
|
||||
return success(hotelZaxLocalService.filterMeta());
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/filter-meta",
|
||||
body == null ? new HashMap<>() : body)));
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
@PostMapping("/detail")
|
||||
@Operation(summary = "酒店详情(优先本地)")
|
||||
public CommonResult<Object> detail(@RequestBody Map<String, Object> body) {
|
||||
String hotelCode = body == null ? null : str(body.get("hotelCode"));
|
||||
Map<String, Object> local = hotelZaxLocalService.detail(hotelCode);
|
||||
if (local != null) {
|
||||
return success(local);
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/detail", body)));
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
@PostMapping("/daily")
|
||||
@Operation(summary = "价态日历(有本地日历用本地,否则供应方)")
|
||||
public CommonResult<Object> daily(@RequestBody Map<String, Object> body) {
|
||||
Map<String, Object> local = hotelZaxLocalService.daily(body);
|
||||
if (local != null) {
|
||||
return success(local);
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("hotel/daily", body)));
|
||||
}
|
||||
|
||||
@PostMapping("/order/check")
|
||||
@Operation(summary = "可订校验")
|
||||
public CommonResult<Object> orderCheck(@RequestBody Map<String, Object> body) {
|
||||
return success(dataOf(zaxHotelOpenClient.post("order/check", body)));
|
||||
}
|
||||
|
||||
@PostMapping("/order/create")
|
||||
@Operation(summary = "创建订单并发起微信支付")
|
||||
public CommonResult<Object> orderCreate(@RequestBody Map<String, Object> body) {
|
||||
return success(hotelZaxTradeService.createOrderAndPay(getLoginUserId(), body));
|
||||
}
|
||||
|
||||
@PostMapping("/order/cancel")
|
||||
@Operation(summary = "取消订单(已支付则退款,并通知资安心)")
|
||||
public CommonResult<Object> orderCancel(@RequestBody Map<String, Object> body) {
|
||||
String channelNo = body == null ? null : str(body.get("channelNo"));
|
||||
hotelZaxTradeService.cancelByChannelNo(getLoginUserId(), channelNo, "用户取消");
|
||||
Map<String, Object> local = hotelZaxTradeService.getByChannelNo(getLoginUserId(), channelNo);
|
||||
return success(local);
|
||||
}
|
||||
|
||||
@PostMapping("/order/page")
|
||||
@Operation(summary = "我的酒店订单分页")
|
||||
public CommonResult<PageResult<Map<String, Object>>> orderPage(
|
||||
@RequestBody(required = false) Map<String, Object> body) {
|
||||
Integer pageNo = body == null || body.get("pageNo") == null ? 1
|
||||
: Integer.valueOf(String.valueOf(body.get("pageNo")));
|
||||
Integer pageSize = body == null || body.get("pageSize") == null ? 10
|
||||
: Integer.valueOf(String.valueOf(body.get("pageSize")));
|
||||
String orderStatus = body == null || body.get("orderStatus") == null ? null
|
||||
: String.valueOf(body.get("orderStatus")).trim();
|
||||
Boolean payStatus = null;
|
||||
if (body != null && body.get("payStatus") != null && !"".equals(String.valueOf(body.get("payStatus")))) {
|
||||
payStatus = Boolean.valueOf(String.valueOf(body.get("payStatus")));
|
||||
}
|
||||
return success(hotelZaxTradeService.pageMyOrders(getLoginUserId(), pageNo, pageSize,
|
||||
StrUtil.blankToDefault(orderStatus, null), payStatus));
|
||||
}
|
||||
|
||||
@PostMapping("/order/query")
|
||||
@Operation(summary = "查询订单(优先本地,再查供应方)")
|
||||
public CommonResult<Object> orderQuery(@RequestBody Map<String, Object> body) {
|
||||
Long uid = getLoginUserId();
|
||||
String channelNo = body == null ? null : str(body.get("channelNo"));
|
||||
Long payOrderId = body == null || body.get("payOrderId") == null ? null
|
||||
: Long.valueOf(String.valueOf(body.get("payOrderId")));
|
||||
Map<String, Object> local = null;
|
||||
if (payOrderId != null) {
|
||||
local = hotelZaxTradeService.getByPayOrderId(uid, payOrderId);
|
||||
}
|
||||
if (local == null && StrUtil.isNotBlank(channelNo)) {
|
||||
local = hotelZaxTradeService.getByChannelNo(uid, channelNo);
|
||||
}
|
||||
if (local != null) {
|
||||
boolean paid = Boolean.TRUE.equals(local.get("payStatus"));
|
||||
boolean booked = local.get("orderNo") != null && StrUtil.isNotBlank(String.valueOf(local.get("orderNo")));
|
||||
if (paid && booked) {
|
||||
try {
|
||||
Map<String, Object> q = new HashMap<>(2);
|
||||
q.put("channelNo", local.get("channelNo"));
|
||||
Object remote = dataOf(zaxHotelOpenClient.post("order/query", q));
|
||||
if (remote instanceof Map) {
|
||||
Map<?, ?> r = (Map<?, ?>) remote;
|
||||
if (r.get("orderStatus") != null) {
|
||||
local.put("orderStatus", r.get("orderStatus"));
|
||||
local.put("status", r.get("orderStatus"));
|
||||
}
|
||||
if (r.get("bookStatus") != null) {
|
||||
local.put("bookStatus", r.get("bookStatus"));
|
||||
}
|
||||
if (r.get("platformNo") != null) {
|
||||
local.put("platformNo", r.get("platformNo"));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
// 本地单仍可展示
|
||||
}
|
||||
}
|
||||
return success(local);
|
||||
}
|
||||
return success(dataOf(zaxHotelOpenClient.post("order/query", body == null ? new HashMap<>() : body)));
|
||||
}
|
||||
|
||||
@PostMapping("/sync")
|
||||
@Operation(summary = "手动触发本地同步(管理/联调)")
|
||||
public CommonResult<Object> sync() {
|
||||
return success(hotelZaxSyncService.syncOnce());
|
||||
}
|
||||
|
||||
private static Object dataOf(JSONObject r) {
|
||||
if (r == null) {
|
||||
return null;
|
||||
}
|
||||
return r.get("data");
|
||||
}
|
||||
|
||||
private static String str(Object o) {
|
||||
return o == null ? null : String.valueOf(o);
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package cn.iocoder.yudao.module.product.controller.app.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenCallbackAuth;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenProperties;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxOrderCallbackService;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxSyncService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.annotation.security.PermitAll;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 资安心 → 小如:开放平台回调接收(与第三方下游约定路径一致,便于把小如当标准样板)。
|
||||
* <p>
|
||||
* 资安心开放应用配置示例:
|
||||
* <ul>
|
||||
* <li>callbackBaseUrl = http://{小如}:{port}/app-api</li>
|
||||
* <li>callbackResourcePath = /hotel/resource/notify</li>
|
||||
* <li>callbackOrderPath = /order/status/notify</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "用户 APP - 资安心酒店开放回调")
|
||||
@RestController
|
||||
@Validated
|
||||
public class HotelZaxOpenCallbackController {
|
||||
|
||||
private static final ExecutorService ASYNC = Executors.newFixedThreadPool(2, r -> {
|
||||
Thread t = new Thread(r, "hotel-zax-callback");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
@Resource
|
||||
private ZaxHotelOpenCallbackAuth callbackAuth;
|
||||
|
||||
@Resource
|
||||
private ZaxHotelOpenProperties openProperties;
|
||||
|
||||
@Resource
|
||||
private HotelZaxSyncService hotelZaxSyncService;
|
||||
|
||||
@Resource
|
||||
private HotelZaxOrderCallbackService orderCallbackService;
|
||||
|
||||
@PermitAll
|
||||
@PostMapping("/hotel/resource/notify")
|
||||
@Operation(summary = "资源变更回调(验签后异步拉 detail)")
|
||||
public CommonResult<Map<String, Object>> resourceNotify(HttpServletRequest request,
|
||||
@RequestBody String rawBody) {
|
||||
callbackAuth.assertValid(request, rawBody);
|
||||
JSONObject body = JSONUtil.parseObj(StrUtil.blankToDefault(rawBody, "{}"));
|
||||
String hotelCode = StrUtil.trim(body.getStr("hotelCode"));
|
||||
String changeType = StrUtil.blankToDefault(body.getStr("changeType"), "HOTEL_UPSERT");
|
||||
if (StrUtil.isBlank(hotelCode)) {
|
||||
throw new IllegalArgumentException("hotelCode 不能为空");
|
||||
}
|
||||
Long tenantId = openProperties.getCallbackTenantId() == null ? 1L : openProperties.getCallbackTenantId();
|
||||
// 先快速 ACK,再异步同步,避免供应方回调超时
|
||||
ASYNC.execute(() -> TenantUtils.execute(tenantId, () -> {
|
||||
try {
|
||||
if ("HOTEL_DELETE".equalsIgnoreCase(changeType) || "HOTEL_OFFLINE".equalsIgnoreCase(changeType)
|
||||
|| "OFFLINE".equalsIgnoreCase(changeType) || "DELETE".equalsIgnoreCase(changeType)) {
|
||||
hotelZaxSyncService.markOffline(hotelCode);
|
||||
}
|
||||
else {
|
||||
hotelZaxSyncService.syncHotelByCode(hotelCode);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-zax-callback|资源同步失败|code={}|type={}|{}", hotelCode, changeType, e.getMessage());
|
||||
}
|
||||
}));
|
||||
Map<String, Object> ack = new LinkedHashMap<>(4);
|
||||
ack.put("hotelCode", hotelCode);
|
||||
ack.put("changeType", changeType);
|
||||
ack.put("accepted", true);
|
||||
return success(ack);
|
||||
}
|
||||
|
||||
@PermitAll
|
||||
@PostMapping({"/order/status/notify", "/hotel/order/status/notify"})
|
||||
@Operation(summary = "订单状态回调")
|
||||
public CommonResult<Map<String, Object>> orderStatusNotify(HttpServletRequest request,
|
||||
@RequestBody String rawBody) {
|
||||
callbackAuth.assertValid(request, rawBody);
|
||||
Long tenantId = openProperties.getCallbackTenantId() == null ? 1L : openProperties.getCallbackTenantId();
|
||||
Map<String, Object> result = TenantUtils.execute(tenantId, () -> orderCallbackService.upsertFromNotify(rawBody));
|
||||
return success(result);
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package cn.iocoder.yudao.module.product.dal.dataobject.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 资安心酒店本地镜像(开放平台同步落库,C 端读本地)。
|
||||
*/
|
||||
@TableName("hotel_zax")
|
||||
@KeySequence("hotel_zax_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HotelZaxDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String hotelName;
|
||||
|
||||
private String groupCode;
|
||||
|
||||
private String coverImage;
|
||||
|
||||
private String carouselImages;
|
||||
|
||||
private String address;
|
||||
|
||||
private String district;
|
||||
|
||||
private Long districtAdcode;
|
||||
|
||||
private String bizArea;
|
||||
|
||||
private Integer starLevel;
|
||||
|
||||
private BigDecimal score;
|
||||
|
||||
private String facilityTags;
|
||||
|
||||
private BigDecimal longitude;
|
||||
|
||||
private BigDecimal latitude;
|
||||
|
||||
private String phone;
|
||||
|
||||
private BigDecimal minSellPrice;
|
||||
|
||||
/** ACTIVE / OFFLINE */
|
||||
private String status;
|
||||
|
||||
private String intro;
|
||||
|
||||
private String roomsJson;
|
||||
|
||||
private String productsJson;
|
||||
|
||||
private LocalDateTime supplierUpdateTime;
|
||||
|
||||
private LocalDateTime syncedAt;
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package cn.iocoder.yudao.module.product.dal.dataobject.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 对齐供应侧 canteen_hotel_daily。
|
||||
*/
|
||||
@TableName("hotel_zax_daily")
|
||||
@KeySequence("hotel_zax_daily_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HotelZaxDailyDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private LocalDate stayDate;
|
||||
|
||||
private BigDecimal costPrice;
|
||||
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
private Integer stock;
|
||||
|
||||
private Integer sold;
|
||||
|
||||
/** 0关 1开 */
|
||||
private Integer dailyStatus;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package cn.iocoder.yudao.module.product.dal.dataobject.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 小如酒店订单(供应侧下单 + 本地微信支付)。
|
||||
*/
|
||||
@TableName("hotel_zax_order")
|
||||
@KeySequence("hotel_zax_order_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HotelZaxOrderDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String channelNo;
|
||||
|
||||
private String orderNo;
|
||||
|
||||
private String platformNo;
|
||||
|
||||
private String orderStatus;
|
||||
|
||||
private String bookStatus;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String hotelName;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private String productName;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private LocalDate checkInDate;
|
||||
|
||||
private LocalDate checkOutDate;
|
||||
|
||||
private Integer nights;
|
||||
|
||||
private Integer roomCount;
|
||||
|
||||
private String contactName;
|
||||
|
||||
private String contactPhone;
|
||||
|
||||
private String guestJson;
|
||||
|
||||
private Long payOrderId;
|
||||
|
||||
private Boolean payStatus;
|
||||
|
||||
private LocalDateTime payTime;
|
||||
|
||||
private BigDecimal sellAmount;
|
||||
|
||||
private String failMsg;
|
||||
|
||||
private String payloadJson;
|
||||
|
||||
private LocalDateTime lastNotifyAt;
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package cn.iocoder.yudao.module.product.dal.dataobject.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 对齐供应侧 canteen_hotel_product。
|
||||
*/
|
||||
@TableName("hotel_zax_product")
|
||||
@KeySequence("hotel_zax_product_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HotelZaxProductDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String groupCode;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private String productName;
|
||||
|
||||
private String priceCode;
|
||||
|
||||
private String cancelCode;
|
||||
|
||||
private Integer mealType;
|
||||
|
||||
private Integer mealCount;
|
||||
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
private Integer stock;
|
||||
|
||||
private String rawJson;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.yudao.module.product.dal.dataobject.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 对齐供应侧 canteen_hotel_room。
|
||||
*/
|
||||
@TableName("hotel_zax_room")
|
||||
@KeySequence("hotel_zax_room_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HotelZaxRoomDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String hotelCode;
|
||||
|
||||
private String groupCode;
|
||||
|
||||
private String roomTypeCode;
|
||||
|
||||
private String roomTypeName;
|
||||
|
||||
private String bedType;
|
||||
|
||||
private Integer capacity;
|
||||
|
||||
private Integer roomNumber;
|
||||
|
||||
private String rawJson;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.module.product.dal.dataobject.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@TableName("hotel_zax_sync_cursor")
|
||||
@KeySequence("hotel_zax_sync_cursor_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HotelZaxSyncCursorDO extends BaseDO {
|
||||
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
private String cursorKey;
|
||||
|
||||
private String updatedSince;
|
||||
|
||||
private LocalDateTime lastSyncAt;
|
||||
|
||||
private String lastMsg;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.product.dal.mysql.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDailyDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HotelZaxDailyMapper extends BaseMapperX<HotelZaxDailyDO> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.product.dal.mysql.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HotelZaxMapper extends BaseMapperX<HotelZaxDO> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.product.dal.mysql.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxOrderDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HotelZaxOrderMapper extends BaseMapperX<HotelZaxOrderDO> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.product.dal.mysql.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxProductDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HotelZaxProductMapper extends BaseMapperX<HotelZaxProductDO> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.product.dal.mysql.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxRoomDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HotelZaxRoomMapper extends BaseMapperX<HotelZaxRoomDO> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package cn.iocoder.yudao.module.product.dal.mysql.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxSyncCursorDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HotelZaxSyncCursorMapper extends BaseMapperX<HotelZaxSyncCursorDO> {
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package cn.iocoder.yudao.module.product.framework.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 校验资安心开放平台回调签名(与开放 API 同一套 MD5 规则)。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ZaxHotelOpenCallbackAuth {
|
||||
|
||||
private static final long SKEW_MS = 5L * 60L * 1000L;
|
||||
|
||||
private final ZaxHotelOpenProperties properties;
|
||||
|
||||
public void assertValid(HttpServletRequest request, String rawBody) {
|
||||
if (!properties.isEnabled()) {
|
||||
throw new IllegalStateException("酒店中转未启用");
|
||||
}
|
||||
String appKey = StrUtil.trim(request.getHeader("appKey"));
|
||||
String timestamp = StrUtil.trim(request.getHeader("timestamp"));
|
||||
String nonce = StrUtil.trim(request.getHeader("nonce"));
|
||||
String sign = StrUtil.trim(request.getHeader("sign"));
|
||||
if (StrUtil.hasBlank(appKey, timestamp, nonce, sign)) {
|
||||
throw new IllegalArgumentException("缺少鉴权头 appKey/timestamp/nonce/sign");
|
||||
}
|
||||
if (!appKey.equals(properties.getAppKey())) {
|
||||
throw new IllegalArgumentException("appKey 不匹配");
|
||||
}
|
||||
long ts;
|
||||
try {
|
||||
ts = Long.parseLong(timestamp);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalArgumentException("timestamp 无效");
|
||||
}
|
||||
if (Math.abs(System.currentTimeMillis() - ts) > SKEW_MS) {
|
||||
throw new IllegalArgumentException("timestamp 超出允许时差");
|
||||
}
|
||||
String body = StrUtil.isBlank(rawBody) ? "{}" : rawBody;
|
||||
String expect = sign(properties.getAppKey(), properties.getAppSecret(), timestamp, nonce, body);
|
||||
if (!expect.equalsIgnoreCase(sign)) {
|
||||
throw new IllegalArgumentException("签名校验失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static String sign(String appKey, String appSecret, String timestamp, String nonce, String bodyJson) {
|
||||
Map<String, String> params = new TreeMap<>();
|
||||
params.put("appKey", StrUtil.nullToEmpty(appKey));
|
||||
params.put("body", StrUtil.nullToEmpty(bodyJson));
|
||||
params.put("nonce", StrUtil.nullToEmpty(nonce));
|
||||
params.put("timestamp", StrUtil.nullToEmpty(timestamp));
|
||||
String stringA = params.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("&"));
|
||||
return DigestUtil.md5Hex(stringA + "&appSecret=" + StrUtil.nullToEmpty(appSecret)).toUpperCase();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package cn.iocoder.yudao.module.product.framework.hotel;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 调用资安心酒店下游开放 API(服务端代签)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ZaxHotelOpenClient {
|
||||
|
||||
private final ZaxHotelOpenProperties properties;
|
||||
|
||||
public JSONObject post(String apiPath, Map<String, Object> body) {
|
||||
if (!properties.isEnabled()) {
|
||||
throw new IllegalStateException("酒店中转未启用,请配置 xrhome.hotel.zax.enabled=true");
|
||||
}
|
||||
if (StrUtil.hasBlank(properties.getBaseUrl(), properties.getAppKey(), properties.getAppSecret())) {
|
||||
throw new IllegalStateException("酒店中转缺少 baseUrl/appKey/appSecret 配置");
|
||||
}
|
||||
String bodyJson = body == null || body.isEmpty() ? "{}" : JSONUtil.toJsonStr(body);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = IdUtil.fastSimpleUUID();
|
||||
String sign = sign(properties.getAppKey(), properties.getAppSecret(), timestamp, nonce, bodyJson);
|
||||
String url = trimSlash(properties.getBaseUrl()) + "/" + StrUtil.removePrefix(apiPath, "/");
|
||||
try (HttpResponse resp = HttpRequest.post(url)
|
||||
.header("appKey", properties.getAppKey())
|
||||
.header("timestamp", timestamp)
|
||||
.header("nonce", nonce)
|
||||
.header("sign", sign)
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.body(bodyJson)
|
||||
.setConnectionTimeout(properties.getConnectTimeoutMs())
|
||||
.setReadTimeout(properties.getReadTimeoutMs())
|
||||
.execute()) {
|
||||
String raw = resp.body();
|
||||
if (!resp.isOk()) {
|
||||
log.warn("|zax-hotel|HTTP失败|url={}|status={}|body={}|", url, resp.getStatus(),
|
||||
StrUtil.maxLength(raw, 300));
|
||||
throw new IllegalStateException("酒店供应方接口异常 HTTP " + resp.getStatus());
|
||||
}
|
||||
JSONObject json = JSONUtil.parseObj(raw);
|
||||
// 资安心 R:code=0 成功
|
||||
Integer code = json.getInt("code");
|
||||
if (code != null && code != 0) {
|
||||
String msg = StrUtil.blankToDefault(json.getStr("msg"), "酒店供应方业务失败");
|
||||
log.warn("|zax-hotel|业务失败|url={}|code={}|msg={}|", url, code, StrUtil.maxLength(msg, 200));
|
||||
throw new IllegalStateException("供应方下单失败:" + msg);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
private static String sign(String appKey, String appSecret, String timestamp, String nonce, String bodyJson) {
|
||||
Map<String, String> params = new TreeMap<>();
|
||||
params.put("appKey", StrUtil.nullToEmpty(appKey));
|
||||
params.put("body", StrUtil.nullToEmpty(bodyJson));
|
||||
params.put("nonce", StrUtil.nullToEmpty(nonce));
|
||||
params.put("timestamp", StrUtil.nullToEmpty(timestamp));
|
||||
String stringA = params.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("&"));
|
||||
return DigestUtil.md5Hex(stringA + "&appSecret=" + StrUtil.nullToEmpty(appSecret)).toUpperCase();
|
||||
}
|
||||
|
||||
private static String trimSlash(String url) {
|
||||
String u = StrUtil.trim(url);
|
||||
while (u.endsWith("/")) {
|
||||
u = u.substring(0, u.length() - 1);
|
||||
}
|
||||
return u;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package cn.iocoder.yudao.module.product.framework.hotel;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 对接资安心酒店开放平台(下游应用凭证放服务端,勿下发小程序)。
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "xrhome.hotel.zax")
|
||||
public class ZaxHotelOpenProperties {
|
||||
|
||||
/** 是否启用中转 */
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 开放平台根地址,必须含到 /third/party/open(不要只配到 /api/supermarket)。
|
||||
* 例:http://localhost:9999/api/supermarket/third/party/open
|
||||
* 拼上 hotel/list 后才是完整路径。
|
||||
*/
|
||||
private String baseUrl = "";
|
||||
|
||||
private String appKey = "";
|
||||
|
||||
private String appSecret = "";
|
||||
|
||||
/** 连接超时毫秒 */
|
||||
private int connectTimeoutMs = 5000;
|
||||
|
||||
/** 读超时毫秒 */
|
||||
private int readTimeoutMs = 20000;
|
||||
|
||||
/**
|
||||
* 供应方回调落库使用的租户(回调无租户头时用此值)。
|
||||
* 与小如运营租户一致,默认 1。
|
||||
*/
|
||||
private Long callbackTenantId = 1L;
|
||||
|
||||
/**
|
||||
* 微信支付应用标识(支付管理→应用信息)。
|
||||
* 默认复用商城 mall;支付回调若命中「交易订单不存在」会再尝试酒店单。
|
||||
*/
|
||||
private String payAppKey = "mall";
|
||||
|
||||
/**
|
||||
* 同步详情时顺带拉取日历的天数(含今天)。0=不同步日历;最大 90(开放平台窗口上限)。
|
||||
*/
|
||||
private int dailySyncDays = 90;
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package cn.iocoder.yudao.module.product.job.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenProperties;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxSyncService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 每天从资安心拉取酒店主数据、房型、产品与价态日历到本地。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HotelZaxDailySyncJob implements JobHandler {
|
||||
|
||||
@Resource
|
||||
private HotelZaxSyncService hotelZaxSyncService;
|
||||
|
||||
@Resource
|
||||
private ZaxHotelOpenProperties openProperties;
|
||||
|
||||
@Override
|
||||
@TenantJob
|
||||
public String execute(String param) {
|
||||
boolean full = !StrUtil.equalsIgnoreCase(StrUtil.trim(param), "incr");
|
||||
Map<String, Object> r = hotelZaxSyncService.syncOnce(full);
|
||||
return formatResult(full, r);
|
||||
}
|
||||
|
||||
/** 每天 03:20 全量同步(适配授权范围变更;增量水位拉不到新授权酒店) */
|
||||
@Scheduled(cron = "0 20 3 * * ?")
|
||||
public void scheduleDaily() {
|
||||
if (!openProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
Long tenantId = openProperties.getCallbackTenantId() == null ? 1L : openProperties.getCallbackTenantId();
|
||||
TenantUtils.execute(tenantId, () -> {
|
||||
try {
|
||||
Map<String, Object> r = hotelZaxSyncService.syncOnce(true);
|
||||
log.info("|hotel-zax-sync|每日定时全量|{}", formatResult(true, r));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-zax-sync|每日定时失败|{}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String formatResult(boolean full, Map<String, Object> r) {
|
||||
if (r == null) {
|
||||
return full ? "全量同步完成" : "增量同步完成";
|
||||
}
|
||||
return String.format("%s 写入%s 详情%s 日历%s条 日历失败%s 下架授权外%s",
|
||||
full ? "全量" : "增量",
|
||||
r.get("upsert"),
|
||||
r.get("detailOk"),
|
||||
r.get("dailyRows"),
|
||||
r.get("dailyFail"),
|
||||
r.get("offlineOutOfScope"));
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package cn.iocoder.yudao.module.product.job.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxTradeService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 酒店待支付超时(10 分钟)自动取消。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HotelZaxUnpaidCancelJob implements JobHandler {
|
||||
|
||||
@Resource
|
||||
private HotelZaxTradeService hotelZaxTradeService;
|
||||
|
||||
@Override
|
||||
@TenantJob
|
||||
public String execute(String param) {
|
||||
int count = hotelZaxTradeService.cancelUnpaidTimeout();
|
||||
return String.format("酒店超时未支付取消 %s 个", count);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "20 * * * * ?")
|
||||
public void schedule() {
|
||||
TenantUtils.executeIgnore(() -> {
|
||||
try {
|
||||
int n = hotelZaxTradeService.cancelUnpaidTimeout();
|
||||
if (n > 0) {
|
||||
log.info("|hotel-pay|定时取消超时未支付|count={}|", n);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-pay|定时取消失败|{}", e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyBatchReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailySaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxOrderPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxOrderRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxProductRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxProductSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRoomRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRoomSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxStatusReqVO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 小如后台:本地酒店维护(展示/编辑对齐供应侧后台能力)。
|
||||
*/
|
||||
public interface HotelZaxAdminService {
|
||||
|
||||
PageResult<HotelZaxRespVO> page(HotelZaxPageReqVO req);
|
||||
|
||||
HotelZaxRespVO getDetail(Long id);
|
||||
|
||||
void update(HotelZaxSaveReqVO req);
|
||||
|
||||
void updateStatus(HotelZaxStatusReqVO req);
|
||||
|
||||
HotelZaxRoomRespVO saveRoom(HotelZaxRoomSaveReqVO req);
|
||||
|
||||
void removeRoom(Long hotelId, Long roomId);
|
||||
|
||||
HotelZaxProductRespVO saveProduct(HotelZaxProductSaveReqVO req);
|
||||
|
||||
void removeProduct(Long hotelId, Long productId);
|
||||
|
||||
PageResult<HotelZaxDailyRespVO> pageDaily(HotelZaxDailyPageReqVO req);
|
||||
|
||||
HotelZaxDailyRespVO saveDaily(HotelZaxDailySaveReqVO req);
|
||||
|
||||
void removeDaily(Long hotelId, Long dailyId);
|
||||
|
||||
Map<String, Object> batchDaily(HotelZaxDailyBatchReqVO req);
|
||||
|
||||
PageResult<HotelZaxOrderRespVO> pageOrders(HotelZaxOrderPageReqVO req);
|
||||
|
||||
HotelZaxOrderRespVO getOrder(Long id);
|
||||
|
||||
/** 后台取消:已支付原路退款,并通知资安心取消预订 */
|
||||
void cancelOrder(Long id, String reason);
|
||||
}
|
||||
+657
@@ -0,0 +1,657 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyBatchReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailyRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxDailySaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxOrderPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxOrderRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxPageReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxProductRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxProductSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRoomRespVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxRoomSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxSaveReqVO;
|
||||
import cn.iocoder.yudao.module.product.controller.admin.hotel.vo.HotelZaxStatusReqVO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDailyDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxOrderDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxProductDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxRoomDO;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxDailyMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxOrderMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxProductMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxRoomMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_DAILY_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_PRODUCT_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ROOM_NOT_EXISTS;
|
||||
|
||||
@Service
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
public class HotelZaxAdminServiceImpl implements HotelZaxAdminService {
|
||||
|
||||
private final HotelZaxMapper hotelZaxMapper;
|
||||
|
||||
private final HotelZaxRoomMapper roomMapper;
|
||||
|
||||
private final HotelZaxProductMapper productMapper;
|
||||
|
||||
private final HotelZaxDailyMapper dailyMapper;
|
||||
|
||||
private final HotelZaxOrderMapper orderMapper;
|
||||
|
||||
private final HotelZaxTradeService hotelZaxTradeService;
|
||||
|
||||
private static final int LOCAL_DAILY_BATCH_MAX_DAYS = 366;
|
||||
|
||||
@Override
|
||||
public PageResult<HotelZaxRespVO> page(HotelZaxPageReqVO req) {
|
||||
PageResult<HotelZaxDO> page = hotelZaxMapper.selectPage(req, new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.likeIfPresent(HotelZaxDO::getHotelName, req.getHotelName())
|
||||
.eqIfPresent(HotelZaxDO::getHotelCode, req.getHotelCode())
|
||||
.eqIfPresent(HotelZaxDO::getStatus, req.getStatus())
|
||||
.eqIfPresent(HotelZaxDO::getDistrictAdcode, req.getDistrictAdcode())
|
||||
.likeIfPresent(HotelZaxDO::getDistrict, req.getDistrict())
|
||||
.orderByDesc(HotelZaxDO::getId));
|
||||
PageResult<HotelZaxRespVO> result = BeanUtils.toBean(page, HotelZaxRespVO.class);
|
||||
fillCounts(result.getList());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelZaxRespVO getDetail(Long id) {
|
||||
HotelZaxDO hotel = requireHotel(id);
|
||||
HotelZaxRespVO vo = BeanUtils.toBean(hotel, HotelZaxRespVO.class);
|
||||
List<HotelZaxRoomDO> rooms = roomMapper.selectList(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.eq(HotelZaxRoomDO::getHotelId, id)
|
||||
.orderByAsc(HotelZaxRoomDO::getRoomTypeCode));
|
||||
List<HotelZaxProductDO> products = productMapper.selectList(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelId, id)
|
||||
.orderByAsc(HotelZaxProductDO::getProductCode));
|
||||
vo.setRooms(BeanUtils.toBean(rooms, HotelZaxRoomRespVO.class));
|
||||
vo.setProducts(BeanUtils.toBean(products, HotelZaxProductRespVO.class));
|
||||
vo.setRoomCount(rooms.size());
|
||||
vo.setProductCount(products.size());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(HotelZaxSaveReqVO req) {
|
||||
HotelZaxDO hotel = requireHotel(req.getId());
|
||||
hotel.setHotelName(StrUtil.trim(req.getHotelName()));
|
||||
hotel.setCoverImage(StrUtil.trim(req.getCoverImage()));
|
||||
hotel.setCarouselImages(normalizeCsv(req.getCarouselImages()));
|
||||
hotel.setAddress(StrUtil.trim(req.getAddress()));
|
||||
hotel.setDistrict(StrUtil.trim(req.getDistrict()));
|
||||
hotel.setDistrictAdcode(req.getDistrictAdcode());
|
||||
hotel.setBizArea(cleanJunk(req.getBizArea()));
|
||||
hotel.setStarLevel(req.getStarLevel());
|
||||
hotel.setScore(req.getScore());
|
||||
hotel.setFacilityTags(normalizeCsv(req.getFacilityTags()));
|
||||
hotel.setLongitude(req.getLongitude());
|
||||
hotel.setLatitude(req.getLatitude());
|
||||
hotel.setPhone(StrUtil.trim(req.getPhone()));
|
||||
if (req.getMinSellPrice() != null) {
|
||||
hotel.setMinSellPrice(req.getMinSellPrice());
|
||||
}
|
||||
if (StrUtil.isNotBlank(req.getStatus())) {
|
||||
hotel.setStatus(normalizeStatus(req.getStatus()));
|
||||
}
|
||||
hotel.setIntro(req.getIntro());
|
||||
hotelZaxMapper.updateById(hotel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateStatus(HotelZaxStatusReqVO req) {
|
||||
HotelZaxDO hotel = requireHotel(req.getId());
|
||||
hotel.setStatus(normalizeStatus(req.getStatus()));
|
||||
hotelZaxMapper.updateById(hotel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotelZaxRoomRespVO saveRoom(HotelZaxRoomSaveReqVO req) {
|
||||
HotelZaxDO hotel = requireHotel(req.getHotelId());
|
||||
String code = StrUtil.trim(req.getRoomTypeCode());
|
||||
HotelZaxRoomDO row;
|
||||
if (req.getId() == null) {
|
||||
Long exists = roomMapper.selectCount(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.eq(HotelZaxRoomDO::getHotelId, hotel.getId())
|
||||
.eq(HotelZaxRoomDO::getRoomTypeCode, code));
|
||||
if (exists != null && exists > 0) {
|
||||
throw invalidParamException("房型编码已存在:{}", code);
|
||||
}
|
||||
row = new HotelZaxRoomDO();
|
||||
}
|
||||
else {
|
||||
row = roomMapper.selectById(req.getId());
|
||||
if (row == null || !Objects.equals(row.getHotelId(), hotel.getId())) {
|
||||
throw exception(HOTEL_ZAX_ROOM_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
row.setHotelId(hotel.getId());
|
||||
row.setHotelCode(hotel.getHotelCode());
|
||||
row.setGroupCode(hotel.getGroupCode());
|
||||
row.setRoomTypeCode(code);
|
||||
row.setRoomTypeName(StrUtil.trim(req.getRoomTypeName()));
|
||||
row.setBedType(StrUtil.trim(req.getBedType()));
|
||||
row.setCapacity(req.getCapacity());
|
||||
row.setRoomNumber(req.getRoomNumber());
|
||||
if (req.getId() == null) {
|
||||
roomMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
roomMapper.updateById(row);
|
||||
}
|
||||
refreshHotelRoomsJson(hotel.getId());
|
||||
return BeanUtils.toBean(row, HotelZaxRoomRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeRoom(Long hotelId, Long roomId) {
|
||||
requireHotel(hotelId);
|
||||
HotelZaxRoomDO old = roomMapper.selectById(roomId);
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
throw exception(HOTEL_ZAX_ROOM_NOT_EXISTS);
|
||||
}
|
||||
Long productCnt = productMapper.selectCount(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelId, hotelId)
|
||||
.eq(HotelZaxProductDO::getRoomTypeCode, old.getRoomTypeCode()));
|
||||
if (productCnt != null && productCnt > 0) {
|
||||
throw invalidParamException("请先删除该房型下的产品");
|
||||
}
|
||||
roomMapper.deleteById(roomId);
|
||||
refreshHotelRoomsJson(hotelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotelZaxProductRespVO saveProduct(HotelZaxProductSaveReqVO req) {
|
||||
HotelZaxDO hotel = requireHotel(req.getHotelId());
|
||||
String roomTypeCode = StrUtil.trim(req.getRoomTypeCode());
|
||||
Long roomCnt = roomMapper.selectCount(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.eq(HotelZaxRoomDO::getHotelId, hotel.getId())
|
||||
.eq(HotelZaxRoomDO::getRoomTypeCode, roomTypeCode));
|
||||
if (roomCnt == null || roomCnt <= 0) {
|
||||
throw invalidParamException("所属房型不存在,请先维护房型");
|
||||
}
|
||||
String productCode = StrUtil.trim(req.getProductCode());
|
||||
HotelZaxProductDO row;
|
||||
if (req.getId() == null) {
|
||||
Long exists = productMapper.selectCount(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelId, hotel.getId())
|
||||
.eq(HotelZaxProductDO::getProductCode, productCode));
|
||||
if (exists != null && exists > 0) {
|
||||
throw invalidParamException("产品编码已存在:{}", productCode);
|
||||
}
|
||||
row = new HotelZaxProductDO();
|
||||
}
|
||||
else {
|
||||
row = productMapper.selectById(req.getId());
|
||||
if (row == null || !Objects.equals(row.getHotelId(), hotel.getId())) {
|
||||
throw exception(HOTEL_ZAX_PRODUCT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
row.setHotelId(hotel.getId());
|
||||
row.setHotelCode(hotel.getHotelCode());
|
||||
row.setGroupCode(hotel.getGroupCode());
|
||||
row.setRoomTypeCode(roomTypeCode);
|
||||
row.setProductCode(productCode);
|
||||
row.setProductName(StrUtil.trim(req.getProductName()));
|
||||
row.setPriceCode(StrUtil.trim(req.getPriceCode()));
|
||||
row.setCancelCode(StrUtil.trim(req.getCancelCode()));
|
||||
row.setMealType(req.getMealType());
|
||||
row.setMealCount(req.getMealCount());
|
||||
row.setSellPrice(req.getSellPrice());
|
||||
row.setStock(req.getStock());
|
||||
if (req.getId() == null) {
|
||||
productMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
productMapper.updateById(row);
|
||||
}
|
||||
refreshHotelProductsJson(hotel.getId());
|
||||
return BeanUtils.toBean(row, HotelZaxProductRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeProduct(Long hotelId, Long productId) {
|
||||
requireHotel(hotelId);
|
||||
HotelZaxProductDO old = productMapper.selectById(productId);
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
throw exception(HOTEL_ZAX_PRODUCT_NOT_EXISTS);
|
||||
}
|
||||
dailyMapper.delete(new LambdaQueryWrapperX<HotelZaxDailyDO>()
|
||||
.eq(HotelZaxDailyDO::getHotelId, hotelId)
|
||||
.eq(HotelZaxDailyDO::getProductCode, old.getProductCode()));
|
||||
productMapper.deleteById(productId);
|
||||
refreshHotelProductsJson(hotelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<HotelZaxDailyRespVO> pageDaily(HotelZaxDailyPageReqVO req) {
|
||||
requireHotel(req.getHotelId());
|
||||
LocalDate from = parseDay(req.getStayFrom());
|
||||
LocalDate to = parseDay(req.getStayTo());
|
||||
PageResult<HotelZaxDailyDO> page = dailyMapper.selectPage(req, new LambdaQueryWrapperX<HotelZaxDailyDO>()
|
||||
.eq(HotelZaxDailyDO::getHotelId, req.getHotelId())
|
||||
.eq(StrUtil.isNotBlank(req.getProductCode()), HotelZaxDailyDO::getProductCode,
|
||||
StrUtil.trim(req.getProductCode()))
|
||||
.ge(from != null, HotelZaxDailyDO::getStayDate, from)
|
||||
.le(to != null, HotelZaxDailyDO::getStayDate, to)
|
||||
.orderByAsc(HotelZaxDailyDO::getStayDate)
|
||||
.orderByAsc(HotelZaxDailyDO::getProductCode));
|
||||
PageResult<HotelZaxDailyRespVO> result = BeanUtils.toBean(page, HotelZaxDailyRespVO.class);
|
||||
if (CollUtil.isNotEmpty(result.getList())) {
|
||||
for (HotelZaxDailyRespVO vo : result.getList()) {
|
||||
fillRemain(vo);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HotelZaxDailyRespVO saveDaily(HotelZaxDailySaveReqVO req) {
|
||||
HotelZaxDO hotel = requireHotel(req.getHotelId());
|
||||
HotelZaxProductDO product = requireProduct(hotel.getId(), req.getProductCode());
|
||||
int stock = req.getStock() == null ? 0 : req.getStock();
|
||||
int status = req.getDailyStatus() == null ? 1 : req.getDailyStatus();
|
||||
HotelZaxDailyDO row;
|
||||
if (req.getId() == null) {
|
||||
HotelZaxDailyDO exist = dailyMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDailyDO>()
|
||||
.eq(HotelZaxDailyDO::getHotelId, hotel.getId())
|
||||
.eq(HotelZaxDailyDO::getProductCode, product.getProductCode())
|
||||
.eq(HotelZaxDailyDO::getStayDate, req.getStayDate())
|
||||
.last("limit 1"));
|
||||
if (exist != null) {
|
||||
row = exist;
|
||||
}
|
||||
else {
|
||||
row = new HotelZaxDailyDO();
|
||||
row.setSold(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
row = dailyMapper.selectById(req.getId());
|
||||
if (row == null || !Objects.equals(row.getHotelId(), hotel.getId())) {
|
||||
throw exception(HOTEL_ZAX_DAILY_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
int sold = row.getSold() == null ? 0 : row.getSold();
|
||||
if (stock < sold) {
|
||||
throw invalidParamException("库存不能小于已售 {}", sold);
|
||||
}
|
||||
row.setHotelId(hotel.getId());
|
||||
row.setHotelCode(hotel.getHotelCode());
|
||||
row.setProductId(product.getId());
|
||||
row.setProductCode(product.getProductCode());
|
||||
row.setRoomTypeCode(product.getRoomTypeCode());
|
||||
row.setStayDate(req.getStayDate());
|
||||
row.setCostPrice(req.getCostPrice() == null ? BigDecimal.ZERO : req.getCostPrice());
|
||||
row.setSellPrice(req.getSellPrice());
|
||||
row.setStock(stock);
|
||||
row.setSold(sold);
|
||||
row.setDailyStatus(status);
|
||||
if (row.getId() == null) {
|
||||
dailyMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
dailyMapper.updateById(row);
|
||||
}
|
||||
return toDailyVo(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeDaily(Long hotelId, Long dailyId) {
|
||||
requireHotel(hotelId);
|
||||
HotelZaxDailyDO old = dailyMapper.selectById(dailyId);
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
throw exception(HOTEL_ZAX_DAILY_NOT_EXISTS);
|
||||
}
|
||||
dailyMapper.deleteById(dailyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> batchDaily(HotelZaxDailyBatchReqVO req) {
|
||||
HotelZaxDO hotel = requireHotel(req.getHotelId());
|
||||
if (req.getEndDate().isBefore(req.getStartDate())) {
|
||||
throw invalidParamException("结束日期不能早于开始日期");
|
||||
}
|
||||
long dayCount = req.getEndDate().toEpochDay() - req.getStartDate().toEpochDay() + 1;
|
||||
if (dayCount > LOCAL_DAILY_BATCH_MAX_DAYS) {
|
||||
throw invalidParamException("单次最多生成/调整 {} 天", LOCAL_DAILY_BATCH_MAX_DAYS);
|
||||
}
|
||||
String mode = StrUtil.blankToDefault(req.getMode(), "").trim().toUpperCase(Locale.ROOT);
|
||||
boolean fillMode = "FILL".equals(mode);
|
||||
boolean adjustMode = "ADJUST".equals(mode);
|
||||
if (!fillMode && !adjustMode) {
|
||||
throw invalidParamException("mode 仅支持 FILL(补齐缺失)或 ADJUST(调整已有)");
|
||||
}
|
||||
boolean fillMissing = Boolean.TRUE.equals(req.getFillMissing());
|
||||
if (adjustMode && req.getCostPrice() == null && req.getSellPrice() == null && req.getStock() == null
|
||||
&& req.getDailyStatus() == null && !fillMissing) {
|
||||
throw invalidParamException("调整模式下请至少填写一项:成本价/卖价/配额/房态,或勾选同时补齐缺失");
|
||||
}
|
||||
List<HotelZaxProductDO> products = loadProductsForBatch(hotel.getId(), req.getProductCodes());
|
||||
if (products.isEmpty()) {
|
||||
throw invalidParamException("没有可操作的产品,请先维护产品");
|
||||
}
|
||||
for (HotelZaxProductDO p : products) {
|
||||
if (p.getSellPrice() == null && req.getSellPrice() == null && (fillMode || fillMissing)) {
|
||||
throw invalidParamException("产品 {} 未设置卖价,请先维护产品或在本次填写覆盖卖价", p.getProductCode());
|
||||
}
|
||||
}
|
||||
List<String> productCodes = products.stream().map(HotelZaxProductDO::getProductCode).collect(Collectors.toList());
|
||||
List<HotelZaxDailyDO> existRows = dailyMapper.selectList(new LambdaQueryWrapperX<HotelZaxDailyDO>()
|
||||
.eq(HotelZaxDailyDO::getHotelId, hotel.getId())
|
||||
.in(HotelZaxDailyDO::getProductCode, productCodes)
|
||||
.ge(HotelZaxDailyDO::getStayDate, req.getStartDate())
|
||||
.le(HotelZaxDailyDO::getStayDate, req.getEndDate()));
|
||||
Map<String, HotelZaxDailyDO> existMap = new HashMap<>(Math.max(16, existRows.size() * 2));
|
||||
for (HotelZaxDailyDO d : existRows) {
|
||||
existMap.put(d.getProductCode() + "|" + d.getStayDate(), d);
|
||||
}
|
||||
int inserted = 0;
|
||||
int updated = 0;
|
||||
int skippedExist = 0;
|
||||
int skippedStock = 0;
|
||||
for (HotelZaxProductDO product : products) {
|
||||
for (LocalDate day = req.getStartDate(); !day.isAfter(req.getEndDate()); day = day.plusDays(1)) {
|
||||
String key = product.getProductCode() + "|" + day;
|
||||
HotelZaxDailyDO exist = existMap.get(key);
|
||||
if (exist == null) {
|
||||
if (fillMode || (adjustMode && fillMissing)) {
|
||||
HotelZaxDailyDO row = buildDailyFromProduct(hotel, product, day, req);
|
||||
dailyMapper.insert(row);
|
||||
inserted++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (fillMode) {
|
||||
skippedExist++;
|
||||
continue;
|
||||
}
|
||||
int sold = exist.getSold() == null ? 0 : exist.getSold();
|
||||
Integer newStock = req.getStock() != null ? req.getStock() : exist.getStock();
|
||||
if (newStock == null) {
|
||||
newStock = 0;
|
||||
}
|
||||
if (newStock < sold) {
|
||||
skippedStock++;
|
||||
continue;
|
||||
}
|
||||
if (req.getCostPrice() != null) {
|
||||
exist.setCostPrice(req.getCostPrice());
|
||||
}
|
||||
if (req.getSellPrice() != null) {
|
||||
exist.setSellPrice(req.getSellPrice());
|
||||
}
|
||||
if (req.getStock() != null) {
|
||||
exist.setStock(req.getStock());
|
||||
}
|
||||
if (req.getDailyStatus() != null) {
|
||||
exist.setDailyStatus(req.getDailyStatus());
|
||||
}
|
||||
exist.setSold(sold);
|
||||
dailyMapper.updateById(exist);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
Map<String, Object> out = new LinkedHashMap<>(8);
|
||||
out.put("inserted", inserted);
|
||||
out.put("updated", updated);
|
||||
out.put("skippedExist", skippedExist);
|
||||
out.put("skippedStock", skippedStock);
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<HotelZaxProductDO> loadProductsForBatch(Long hotelId, List<String> productCodes) {
|
||||
LambdaQueryWrapperX<HotelZaxProductDO> w = new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelId, hotelId);
|
||||
if (CollUtil.isNotEmpty(productCodes)) {
|
||||
List<String> codes = productCodes.stream().filter(StrUtil::isNotBlank).map(String::trim).distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (codes.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
w.in(HotelZaxProductDO::getProductCode, codes);
|
||||
}
|
||||
return productMapper.selectList(w);
|
||||
}
|
||||
|
||||
private HotelZaxDailyDO buildDailyFromProduct(HotelZaxDO hotel, HotelZaxProductDO product, LocalDate day,
|
||||
HotelZaxDailyBatchReqVO req) {
|
||||
HotelZaxDailyDO row = new HotelZaxDailyDO();
|
||||
row.setHotelId(hotel.getId());
|
||||
row.setHotelCode(hotel.getHotelCode());
|
||||
row.setProductId(product.getId());
|
||||
row.setProductCode(product.getProductCode());
|
||||
row.setRoomTypeCode(product.getRoomTypeCode());
|
||||
row.setStayDate(day);
|
||||
row.setCostPrice(req.getCostPrice() != null ? req.getCostPrice() : BigDecimal.ZERO);
|
||||
row.setSellPrice(req.getSellPrice() != null ? req.getSellPrice() : product.getSellPrice());
|
||||
row.setStock(req.getStock() != null ? req.getStock() : (product.getStock() == null ? 0 : product.getStock()));
|
||||
row.setSold(0);
|
||||
row.setDailyStatus(req.getDailyStatus() != null ? req.getDailyStatus() : 1);
|
||||
return row;
|
||||
}
|
||||
|
||||
private HotelZaxProductDO requireProduct(Long hotelId, String productCode) {
|
||||
HotelZaxProductDO product = productMapper.selectOne(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelId, hotelId)
|
||||
.eq(HotelZaxProductDO::getProductCode, StrUtil.trim(productCode))
|
||||
.last("limit 1"));
|
||||
if (product == null) {
|
||||
throw invalidParamException("产品不存在,请先维护产品");
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
private static HotelZaxDailyRespVO toDailyVo(HotelZaxDailyDO row) {
|
||||
HotelZaxDailyRespVO vo = BeanUtils.toBean(row, HotelZaxDailyRespVO.class);
|
||||
fillRemain(vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static void fillRemain(HotelZaxDailyRespVO vo) {
|
||||
if (vo == null) {
|
||||
return;
|
||||
}
|
||||
int stock = vo.getStock() == null ? 0 : vo.getStock();
|
||||
int sold = vo.getSold() == null ? 0 : vo.getSold();
|
||||
vo.setRemainStock(Math.max(0, stock - sold));
|
||||
}
|
||||
|
||||
private static LocalDate parseDay(String raw) {
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String s = raw.trim().replace('/', '-');
|
||||
if (s.length() >= 10) {
|
||||
s = s.substring(0, 10);
|
||||
}
|
||||
return LocalDate.parse(s);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void fillCounts(List<HotelZaxRespVO> list) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
List<Long> ids = list.stream().map(HotelZaxRespVO::getId).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<HotelZaxRoomDO> rooms = roomMapper.selectList(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.in(HotelZaxRoomDO::getHotelId, ids)
|
||||
.select(HotelZaxRoomDO::getHotelId));
|
||||
List<HotelZaxProductDO> products = productMapper.selectList(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.in(HotelZaxProductDO::getHotelId, ids)
|
||||
.select(HotelZaxProductDO::getHotelId));
|
||||
Map<Long, Integer> roomMap = new HashMap<>();
|
||||
Map<Long, Integer> productMap = new HashMap<>();
|
||||
for (HotelZaxRoomDO r : rooms) {
|
||||
if (r != null && r.getHotelId() != null) {
|
||||
roomMap.merge(r.getHotelId(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
for (HotelZaxProductDO p : products) {
|
||||
if (p != null && p.getHotelId() != null) {
|
||||
productMap.merge(p.getHotelId(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
for (HotelZaxRespVO vo : list) {
|
||||
vo.setRoomCount(roomMap.getOrDefault(vo.getId(), 0));
|
||||
vo.setProductCount(productMap.getOrDefault(vo.getId(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshHotelRoomsJson(Long hotelId) {
|
||||
HotelZaxDO hotel = hotelZaxMapper.selectById(hotelId);
|
||||
if (hotel == null) {
|
||||
return;
|
||||
}
|
||||
List<HotelZaxRoomDO> rooms = roomMapper.selectList(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.eq(HotelZaxRoomDO::getHotelId, hotelId)
|
||||
.orderByAsc(HotelZaxRoomDO::getRoomTypeCode));
|
||||
List<Map<String, Object>> arr = rooms.stream().map(r -> {
|
||||
Map<String, Object> m = new HashMap<>(6);
|
||||
m.put("roomTypeCode", r.getRoomTypeCode());
|
||||
m.put("roomTypeName", r.getRoomTypeName());
|
||||
m.put("bedType", r.getBedType());
|
||||
m.put("capacity", r.getCapacity());
|
||||
return m;
|
||||
}).collect(Collectors.toList());
|
||||
hotel.setRoomsJson(JSONUtil.toJsonStr(arr));
|
||||
hotelZaxMapper.updateById(hotel);
|
||||
}
|
||||
|
||||
private void refreshHotelProductsJson(Long hotelId) {
|
||||
HotelZaxDO hotel = hotelZaxMapper.selectById(hotelId);
|
||||
if (hotel == null) {
|
||||
return;
|
||||
}
|
||||
List<HotelZaxProductDO> products = productMapper.selectList(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelId, hotelId)
|
||||
.orderByAsc(HotelZaxProductDO::getProductCode));
|
||||
List<Map<String, Object>> arr = products.stream().map(p -> {
|
||||
Map<String, Object> m = new HashMap<>(8);
|
||||
m.put("productCode", p.getProductCode());
|
||||
m.put("productName", p.getProductName());
|
||||
m.put("roomTypeCode", p.getRoomTypeCode());
|
||||
m.put("priceCode", p.getPriceCode());
|
||||
m.put("mealType", p.getMealType());
|
||||
m.put("mealCount", p.getMealCount());
|
||||
m.put("sellPrice", p.getSellPrice());
|
||||
return m;
|
||||
}).collect(Collectors.toList());
|
||||
hotel.setProductsJson(JSONUtil.toJsonStr(arr));
|
||||
hotelZaxMapper.updateById(hotel);
|
||||
}
|
||||
|
||||
private HotelZaxDO requireHotel(Long id) {
|
||||
HotelZaxDO hotel = hotelZaxMapper.selectById(id);
|
||||
if (hotel == null) {
|
||||
throw exception(HOTEL_ZAX_NOT_EXISTS);
|
||||
}
|
||||
return hotel;
|
||||
}
|
||||
|
||||
private static String normalizeStatus(String status) {
|
||||
String s = StrUtil.trim(status);
|
||||
if ("OFFLINE".equalsIgnoreCase(s)) {
|
||||
return "OFFLINE";
|
||||
}
|
||||
return "ACTIVE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<HotelZaxOrderRespVO> pageOrders(HotelZaxOrderPageReqVO req) {
|
||||
PageResult<HotelZaxOrderDO> page = orderMapper.selectPage(req, new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.likeIfPresent(HotelZaxOrderDO::getHotelName, req.getHotelName())
|
||||
.eqIfPresent(HotelZaxOrderDO::getHotelCode, req.getHotelCode())
|
||||
.eqIfPresent(HotelZaxOrderDO::getChannelNo, req.getChannelNo())
|
||||
.eqIfPresent(HotelZaxOrderDO::getOrderNo, req.getOrderNo())
|
||||
.eqIfPresent(HotelZaxOrderDO::getOrderStatus, req.getOrderStatus())
|
||||
.eqIfPresent(HotelZaxOrderDO::getPayStatus, req.getPayStatus())
|
||||
.eqIfPresent(HotelZaxOrderDO::getContactPhone, req.getContactPhone())
|
||||
.eqIfPresent(HotelZaxOrderDO::getUserId, req.getUserId())
|
||||
.orderByDesc(HotelZaxOrderDO::getId));
|
||||
return BeanUtils.toBean(page, HotelZaxOrderRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelZaxOrderRespVO getOrder(Long id) {
|
||||
if (id == null) {
|
||||
throw invalidParamException("订单 id 不能为空");
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectById(id);
|
||||
if (order == null) {
|
||||
throw exception(HOTEL_ZAX_ORDER_NOT_EXISTS);
|
||||
}
|
||||
return BeanUtils.toBean(order, HotelZaxOrderRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelOrder(Long id, String reason) {
|
||||
hotelZaxTradeService.cancelOrder(id, StrUtil.blankToDefault(reason, "后台取消"), true);
|
||||
}
|
||||
|
||||
private static String normalizeCsv(String v) {
|
||||
if (StrUtil.isBlank(v)) {
|
||||
return null;
|
||||
}
|
||||
return StrUtil.splitTrim(v, ',').stream().filter(StrUtil::isNotBlank).collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
private static String cleanJunk(String v) {
|
||||
if (StrUtil.isBlank(v)) {
|
||||
return null;
|
||||
}
|
||||
String s = v.trim();
|
||||
if ("[]".equals(s) || "{}".equals(s) || "()".equals(s)) {
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 小如本地酒店查询(C 端读库)。
|
||||
*/
|
||||
public interface HotelZaxLocalService {
|
||||
|
||||
Map<String, Object> pageList(Map<String, Object> body);
|
||||
|
||||
Map<String, Object> detail(String hotelCode);
|
||||
|
||||
Map<String, Object> filterMeta();
|
||||
|
||||
/** 本地是否已有上架数据 */
|
||||
boolean hasLocalData();
|
||||
|
||||
/**
|
||||
* 本地日历;无本地日历时返回 null,由调用方回退供应方。
|
||||
*/
|
||||
Map<String, Object> daily(Map<String, Object> body);
|
||||
|
||||
/**
|
||||
* 按日历逐夜累计房价(入住日含、离店日不含)× 间数。
|
||||
* 任一夜无开房日历或库存不足则返回 null。
|
||||
*/
|
||||
BigDecimal quoteStay(String hotelCode, String productCode, LocalDate checkIn, LocalDate checkOut, int rooms);
|
||||
|
||||
/** 用已查到的日历行报价,规则同 quoteStay。 */
|
||||
BigDecimal quoteFromDailyRows(List<Map<String, Object>> rows, String productCode, LocalDate checkIn,
|
||||
LocalDate checkOut, int rooms);
|
||||
}
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDailyDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxProductDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxRoomDO;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxDailyMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxProductMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxRoomMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HotelZaxLocalServiceImpl implements HotelZaxLocalService {
|
||||
|
||||
private final HotelZaxMapper hotelZaxMapper;
|
||||
|
||||
private final HotelZaxRoomMapper roomMapper;
|
||||
|
||||
private final HotelZaxProductMapper productMapper;
|
||||
|
||||
private final HotelZaxDailyMapper dailyMapper;
|
||||
|
||||
@Override
|
||||
public boolean hasLocalData() {
|
||||
return hotelZaxMapper.selectCount(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getStatus, "ACTIVE")) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> pageList(Map<String, Object> body) {
|
||||
long current = toLong(body == null ? null : body.get("current"), 1L);
|
||||
long size = Math.min(50L, toLong(body == null ? null : body.get("size"), 10L));
|
||||
String keyword = trim(body == null ? null : body.get("keyword"));
|
||||
String district = trim(body == null ? null : body.get("district"));
|
||||
Long districtAdcode = toLongObj(body == null ? null : body.get("districtAdcode"));
|
||||
String bizArea = trim(body == null ? null : body.get("bizArea"));
|
||||
|
||||
LambdaQueryWrapperX<HotelZaxDO> qw = new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getStatus, "ACTIVE")
|
||||
.likeIfPresent(HotelZaxDO::getHotelName, keyword)
|
||||
.likeIfPresent(HotelZaxDO::getBizArea, bizArea)
|
||||
.orderByDesc(HotelZaxDO::getUpdateTime);
|
||||
if (districtAdcode != null) {
|
||||
// 简化:等值或前缀(省/市码用范围可后续增强)
|
||||
qw.and(w -> w.eq(HotelZaxDO::getDistrictAdcode, districtAdcode)
|
||||
.or()
|
||||
.apply("district_adcode is not null and cast(district_adcode as char) like {0}",
|
||||
String.valueOf(districtAdcode).replaceAll("0+$", "") + "%"));
|
||||
}
|
||||
else if (StrUtil.isNotBlank(district)) {
|
||||
qw.and(w -> w.like(HotelZaxDO::getDistrict, district).or().like(HotelZaxDO::getAddress, district));
|
||||
}
|
||||
|
||||
Page<HotelZaxDO> page = hotelZaxMapper.selectPage(new Page<>(current, size), qw);
|
||||
List<Map<String, Object>> records = page.getRecords().stream().map(this::toBrief).collect(Collectors.toList());
|
||||
Map<String, Object> data = new LinkedHashMap<>(8);
|
||||
data.put("records", records);
|
||||
data.put("total", page.getTotal());
|
||||
data.put("current", page.getCurrent());
|
||||
data.put("size", page.getSize());
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> detail(String hotelCode) {
|
||||
if (StrUtil.isBlank(hotelCode)) {
|
||||
return null;
|
||||
}
|
||||
HotelZaxDO h = hotelZaxMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getHotelCode, hotelCode.trim())
|
||||
.last("limit 1"));
|
||||
if (h == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> data = toBrief(h);
|
||||
data.put("carouselImages", h.getCarouselImages());
|
||||
data.put("intro", h.getIntro());
|
||||
data.put("phone", h.getPhone());
|
||||
List<Map<String, Object>> rooms = loadRooms(h.getHotelCode());
|
||||
List<Map<String, Object>> products = loadProducts(h.getHotelCode());
|
||||
// 明细表优先;未拆表前兼容 JSON 冗余
|
||||
data.put("rooms", rooms.isEmpty() ? parseArray(h.getRoomsJson()) : rooms);
|
||||
data.put("products", products.isEmpty() ? parseArray(h.getProductsJson()) : products);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> daily(Map<String, Object> body) {
|
||||
String hotelCode = trim(body == null ? null : body.get("hotelCode"));
|
||||
if (StrUtil.isBlank(hotelCode)) {
|
||||
return null;
|
||||
}
|
||||
HotelZaxDO h = hotelZaxMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getHotelCode, hotelCode)
|
||||
.last("limit 1"));
|
||||
if (h == null) {
|
||||
return null;
|
||||
}
|
||||
String productCode = trim(body == null ? null : body.get("productCode"));
|
||||
LocalDate start = parseDay(body == null ? null : body.get("startDate"));
|
||||
LocalDate end = parseDay(body == null ? null : body.get("endDate"));
|
||||
if (start == null) {
|
||||
start = LocalDate.now();
|
||||
}
|
||||
if (end == null || end.isBefore(start)) {
|
||||
end = start;
|
||||
}
|
||||
if (start.plusDays(90).isBefore(end)) {
|
||||
end = start.plusDays(90);
|
||||
}
|
||||
List<HotelZaxDailyDO> dailies = dailyMapper.selectList(new LambdaQueryWrapperX<HotelZaxDailyDO>()
|
||||
.eq(HotelZaxDailyDO::getHotelId, h.getId())
|
||||
.eq(StrUtil.isNotBlank(productCode), HotelZaxDailyDO::getProductCode, productCode)
|
||||
.ge(HotelZaxDailyDO::getStayDate, start)
|
||||
.le(HotelZaxDailyDO::getStayDate, end)
|
||||
.orderByAsc(HotelZaxDailyDO::getProductCode)
|
||||
.orderByAsc(HotelZaxDailyDO::getStayDate)
|
||||
.last("limit 5000"));
|
||||
if (dailies == null || dailies.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
for (HotelZaxDailyDO d : dailies) {
|
||||
if (d == null) {
|
||||
continue;
|
||||
}
|
||||
int stock = d.getStock() == null ? 0 : d.getStock();
|
||||
int sold = d.getSold() == null ? 0 : d.getSold();
|
||||
Map<String, Object> row = new LinkedHashMap<>(10);
|
||||
row.put("productCode", d.getProductCode());
|
||||
row.put("roomTypeCode", d.getRoomTypeCode());
|
||||
row.put("stayDate", d.getStayDate() == null ? null : d.getStayDate().toString());
|
||||
row.put("sellPrice", d.getSellPrice());
|
||||
row.put("stock", stock);
|
||||
row.put("sold", sold);
|
||||
row.put("remain", Math.max(0, stock - sold));
|
||||
row.put("dailyStatus", d.getDailyStatus());
|
||||
list.add(row);
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>(4);
|
||||
data.put("hotelCode", hotelCode);
|
||||
data.put("list", list);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal quoteStay(String hotelCode, String productCode, LocalDate checkIn, LocalDate checkOut, int rooms) {
|
||||
if (StrUtil.hasBlank(hotelCode, productCode) || checkIn == null || checkOut == null
|
||||
|| !checkOut.isAfter(checkIn)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> body = new LinkedHashMap<String, Object>(8);
|
||||
body.put("hotelCode", hotelCode);
|
||||
body.put("productCode", productCode);
|
||||
body.put("startDate", checkIn.toString());
|
||||
body.put("endDate", checkOut.minusDays(1).toString());
|
||||
Map<String, Object> data = daily(body);
|
||||
if (data == null || data.get("list") == null) {
|
||||
return null;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> rows = (List<Map<String, Object>>) data.get("list");
|
||||
return quoteFromDailyRows(rows, productCode, checkIn, checkOut, rooms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal quoteFromDailyRows(List<Map<String, Object>> rows, String productCode, LocalDate checkIn,
|
||||
LocalDate checkOut, int rooms) {
|
||||
if (rows == null || StrUtil.isBlank(productCode) || checkIn == null || checkOut == null
|
||||
|| !checkOut.isAfter(checkIn)) {
|
||||
return null;
|
||||
}
|
||||
int need = rooms < 1 ? 1 : rooms;
|
||||
Map<String, Map<String, Object>> byDay = new LinkedHashMap<String, Map<String, Object>>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
if (StrUtil.isNotBlank(productCode) && !productCode.equals(trim(row.get("productCode")))) {
|
||||
continue;
|
||||
}
|
||||
String ymd = stayYmd(row.get("stayDate"));
|
||||
if (StrUtil.isBlank(ymd)) {
|
||||
continue;
|
||||
}
|
||||
byDay.put(ymd, row);
|
||||
}
|
||||
BigDecimal nightSum = BigDecimal.ZERO;
|
||||
for (LocalDate day = checkIn; day.isBefore(checkOut); day = day.plusDays(1)) {
|
||||
Map<String, Object> row = byDay.get(day.toString());
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
Integer status = toIntObj(row.get("dailyStatus"));
|
||||
if (status == null || status.intValue() != 1) {
|
||||
return null;
|
||||
}
|
||||
int remain = remainOf(row);
|
||||
if (remain < need) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal price = toMoney(row.get("sellPrice"));
|
||||
if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return null;
|
||||
}
|
||||
nightSum = nightSum.add(price);
|
||||
}
|
||||
return nightSum.multiply(new BigDecimal(need)).setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private static String stayYmd(Object raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
String s = String.valueOf(raw).trim().replace('/', '-');
|
||||
if (s.length() >= 10) {
|
||||
s = s.substring(0, 10);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static int remainOf(Map<String, Object> row) {
|
||||
Object remain = row.get("remain");
|
||||
if (remain == null) {
|
||||
remain = row.get("remainStock");
|
||||
}
|
||||
if (remain != null) {
|
||||
Integer n = toIntObj(remain);
|
||||
return n == null ? 0 : Math.max(0, n.intValue());
|
||||
}
|
||||
int stock = toIntObj(row.get("stock")) == null ? 0 : toIntObj(row.get("stock")).intValue();
|
||||
int sold = toIntObj(row.get("sold")) == null ? 0 : toIntObj(row.get("sold")).intValue();
|
||||
return Math.max(0, stock - sold);
|
||||
}
|
||||
|
||||
private static Integer toIntObj(Object o) {
|
||||
if (o == null || StrUtil.isBlank(String.valueOf(o))) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(new BigDecimal(String.valueOf(o).trim()).intValue());
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal toMoney(Object o) {
|
||||
if (o == null || StrUtil.isBlank(String.valueOf(o))) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(o).trim());
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalDate parseDay(Object raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String s = String.valueOf(raw).trim().replace('/', '-');
|
||||
if (s.length() >= 10) {
|
||||
s = s.substring(0, 10);
|
||||
}
|
||||
return LocalDate.parse(s);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> loadRooms(String hotelCode) {
|
||||
List<HotelZaxRoomDO> list = roomMapper.selectList(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.eq(HotelZaxRoomDO::getHotelCode, hotelCode)
|
||||
.orderByAsc(HotelZaxRoomDO::getRoomTypeCode));
|
||||
List<Map<String, Object>> out = new ArrayList<>();
|
||||
for (HotelZaxRoomDO r : list) {
|
||||
if (r == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> row = new LinkedHashMap<>(6);
|
||||
row.put("roomTypeCode", r.getRoomTypeCode());
|
||||
row.put("roomTypeName", r.getRoomTypeName());
|
||||
row.put("bedType", r.getBedType());
|
||||
row.put("capacity", r.getCapacity());
|
||||
out.add(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> loadProducts(String hotelCode) {
|
||||
List<HotelZaxProductDO> list = productMapper.selectList(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelCode, hotelCode)
|
||||
.orderByAsc(HotelZaxProductDO::getProductCode));
|
||||
List<Map<String, Object>> out = new ArrayList<>();
|
||||
for (HotelZaxProductDO p : list) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> row = new LinkedHashMap<>(8);
|
||||
row.put("productCode", p.getProductCode());
|
||||
row.put("productName", p.getProductName());
|
||||
row.put("roomTypeCode", p.getRoomTypeCode());
|
||||
row.put("priceCode", p.getPriceCode());
|
||||
row.put("mealType", p.getMealType());
|
||||
row.put("mealCount", p.getMealCount());
|
||||
row.put("sellPrice", p.getSellPrice());
|
||||
out.add(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> filterMeta() {
|
||||
List<HotelZaxDO> rows = hotelZaxMapper.selectList(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.select(HotelZaxDO::getDistrict, HotelZaxDO::getDistrictAdcode, HotelZaxDO::getBizArea)
|
||||
.eq(HotelZaxDO::getStatus, "ACTIVE"));
|
||||
Map<Long, String> areaByAdcode = new LinkedHashMap<>();
|
||||
Set<String> districts = new LinkedHashSet<>();
|
||||
Set<String> bizAreas = new LinkedHashSet<>();
|
||||
for (HotelZaxDO h : rows) {
|
||||
if (h == null) {
|
||||
continue;
|
||||
}
|
||||
if (StrUtil.isNotBlank(h.getDistrict())) {
|
||||
districts.add(h.getDistrict().trim());
|
||||
}
|
||||
if (h.getDistrictAdcode() != null) {
|
||||
areaByAdcode.putIfAbsent(h.getDistrictAdcode(),
|
||||
StrUtil.blankToDefault(h.getDistrict(), String.valueOf(h.getDistrictAdcode())));
|
||||
}
|
||||
if (StrUtil.isNotBlank(h.getBizArea()) && !"[]".equals(h.getBizArea().trim())
|
||||
&& !"{}".equals(h.getBizArea().trim())) {
|
||||
bizAreas.add(h.getBizArea().trim());
|
||||
}
|
||||
}
|
||||
List<Map<String, Object>> areas = areaByAdcode.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(e -> {
|
||||
Map<String, Object> one = new LinkedHashMap<>(4);
|
||||
one.put("districtAdcode", e.getKey());
|
||||
one.put("district", e.getValue());
|
||||
return one;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
List<Map<String, Object>> starLevels = new ArrayList<>();
|
||||
starLevels.add(starItem(5, "五星"));
|
||||
starLevels.add(starItem(4, "高档"));
|
||||
starLevels.add(starItem(3, "舒适"));
|
||||
starLevels.add(starItem(2, "经济"));
|
||||
starLevels.add(starItem(1, "民宿"));
|
||||
Map<String, Object> data = new LinkedHashMap<>(8);
|
||||
data.put("areas", areas);
|
||||
data.put("districts", new ArrayList<>(districts));
|
||||
data.put("bizAreas", new ArrayList<>(bizAreas));
|
||||
data.put("starLevels", starLevels);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static Map<String, Object> starItem(int value, String label) {
|
||||
Map<String, Object> one = new LinkedHashMap<>(2);
|
||||
one.put("value", value);
|
||||
one.put("label", label);
|
||||
return one;
|
||||
}
|
||||
|
||||
private Map<String, Object> toBrief(HotelZaxDO h) {
|
||||
Map<String, Object> row = new LinkedHashMap<>(16);
|
||||
row.put("hotelCode", h.getHotelCode());
|
||||
row.put("hotelName", h.getHotelName());
|
||||
row.put("coverImage", h.getCoverImage());
|
||||
row.put("address", h.getAddress());
|
||||
row.put("district", h.getDistrict());
|
||||
row.put("districtAdcode", h.getDistrictAdcode());
|
||||
row.put("bizArea", h.getBizArea());
|
||||
row.put("starLevel", h.getStarLevel());
|
||||
row.put("score", h.getScore());
|
||||
row.put("facilityTags", h.getFacilityTags());
|
||||
row.put("minSellPrice", h.getMinSellPrice());
|
||||
row.put("status", h.getStatus());
|
||||
return row;
|
||||
}
|
||||
|
||||
private static List<?> parseArray(String json) {
|
||||
if (StrUtil.isBlank(json)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return JSONUtil.parseArray(json);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(Object o) {
|
||||
return o == null ? null : StrUtil.trim(String.valueOf(o));
|
||||
}
|
||||
|
||||
private static long toLong(Object o, long def) {
|
||||
if (o == null) {
|
||||
return def;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(o));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static Long toLongObj(Object o) {
|
||||
if (o == null || StrUtil.isBlank(String.valueOf(o))) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(o).trim());
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 供应方订单状态回调落库。
|
||||
*/
|
||||
public interface HotelZaxOrderCallbackService {
|
||||
|
||||
/**
|
||||
* @param rawBody 原始 JSON(验签后)
|
||||
* @return 落库摘要
|
||||
*/
|
||||
Map<String, Object> upsertFromNotify(String rawBody);
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HotelZaxOrderCallbackServiceImpl implements HotelZaxOrderCallbackService {
|
||||
|
||||
private final HotelZaxTradeService hotelZaxTradeService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> upsertFromNotify(String rawBody) {
|
||||
return hotelZaxTradeService.applySupplierStatusNotify(JSONUtil.parseObj(StrUtil.blankToDefault(rawBody, "{}")));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 从资安心开放平台增量同步到本地库。
|
||||
*/
|
||||
public interface HotelZaxSyncService {
|
||||
|
||||
/**
|
||||
* 拉取一轮同步(默认全量,适配授权范围变更)。
|
||||
* @return 统计信息
|
||||
*/
|
||||
Map<String, Object> syncOnce();
|
||||
|
||||
/**
|
||||
* @param full true=忽略水位按当前授权全量拉;false=按水位增量
|
||||
*/
|
||||
Map<String, Object> syncOnce(boolean full);
|
||||
|
||||
/**
|
||||
* 单店拉取详情并落库(资源回调触发)。
|
||||
*/
|
||||
Map<String, Object> syncHotelByCode(String hotelCode);
|
||||
|
||||
/**
|
||||
* 标记下架/删除(资源回调 HOTEL_OFFLINE / HOTEL_DELETE)。
|
||||
*/
|
||||
void markOffline(String hotelCode);
|
||||
|
||||
/**
|
||||
* 按本地酒店 id 拉取资安心价态日历并落库。
|
||||
*/
|
||||
Map<String, Object> syncDailyByHotelId(Long hotelId);
|
||||
}
|
||||
+600
@@ -0,0 +1,600 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxDailyDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxProductDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxRoomDO;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxSyncCursorDO;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxDailyMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxProductMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxRoomMapper;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxSyncCursorMapper;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenClient;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 开放平台 hotel/sync + detail → 本地 hotel_zax / room / product(结构对齐供应侧)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
|
||||
|
||||
private static final String CURSOR_KEY = "default";
|
||||
|
||||
private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final ZaxHotelOpenClient openClient;
|
||||
|
||||
private final ZaxHotelOpenProperties openProperties;
|
||||
|
||||
private final HotelZaxMapper hotelZaxMapper;
|
||||
|
||||
private final HotelZaxRoomMapper roomMapper;
|
||||
|
||||
private final HotelZaxProductMapper productMapper;
|
||||
|
||||
private final HotelZaxDailyMapper dailyMapper;
|
||||
|
||||
private final HotelZaxSyncCursorMapper cursorMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> syncOnce() {
|
||||
// 管理端「同步资安心」默认全量:改授权范围后增量水位拉不到新授权酒店
|
||||
return syncOnce(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> syncOnce(boolean full) {
|
||||
if (!openProperties.isEnabled()) {
|
||||
throw new IllegalStateException("酒店中转未启用");
|
||||
}
|
||||
HotelZaxSyncCursorDO cursor = getOrCreateCursor();
|
||||
// 全量忽略水位;增量才带 updatedSince
|
||||
String updatedSince = full ? null : cursor.getUpdatedSince();
|
||||
int pageNo = 1;
|
||||
int upsert = 0;
|
||||
int detailOk = 0;
|
||||
int dailyOk = 0;
|
||||
int dailyFail = 0;
|
||||
int dailyRows = 0;
|
||||
int offlineOutOfScope = 0;
|
||||
String nextSince = updatedSince;
|
||||
boolean hasMore = true;
|
||||
boolean pullOk = false;
|
||||
Set<String> syncedCodes = new HashSet<>();
|
||||
while (hasMore && pageNo <= 40) {
|
||||
Map<String, Object> req = new HashMap<>();
|
||||
req.put("current", pageNo);
|
||||
req.put("size", 50);
|
||||
req.put("includeOffline", true);
|
||||
if (StrUtil.isNotBlank(updatedSince)) {
|
||||
req.put("updatedSince", updatedSince);
|
||||
}
|
||||
JSONObject resp = openClient.post("hotel/sync", req);
|
||||
JSONObject data = resp == null ? null : resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
break;
|
||||
}
|
||||
pullOk = true;
|
||||
JSONArray records = data.getJSONArray("records");
|
||||
if (records == null || records.isEmpty()) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
JSONObject row = records.getJSONObject(i);
|
||||
if (row == null || StrUtil.isBlank(row.getStr("hotelCode"))) {
|
||||
continue;
|
||||
}
|
||||
String code = row.getStr("hotelCode").trim();
|
||||
syncedCodes.add(code);
|
||||
upsertHotelBrief(row);
|
||||
upsert++;
|
||||
try {
|
||||
DetailPullResult dr = pullDetailResult(code);
|
||||
if (dr.ok) {
|
||||
detailOk++;
|
||||
}
|
||||
dailyRows += dr.dailyRows;
|
||||
if (dr.dailyFail) {
|
||||
dailyFail++;
|
||||
}
|
||||
else if (dr.ok) {
|
||||
dailyOk++;
|
||||
}
|
||||
sleepQuiet(80L);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-zax-sync|detail失败|code={}|{}", code, e.getMessage());
|
||||
dailyFail++;
|
||||
}
|
||||
}
|
||||
String ns = data.getStr("nextUpdatedSince");
|
||||
if (StrUtil.isNotBlank(ns)) {
|
||||
nextSince = ns;
|
||||
}
|
||||
hasMore = Boolean.TRUE.equals(data.getBool("hasMore"));
|
||||
pageNo++;
|
||||
}
|
||||
// 全量且拉取成功后:本地有、当前授权已不含的酒店标记下架(避免改范围后仍显示旧数据)
|
||||
// pullOk=false 时不清理,防止接口异常把本地全下架
|
||||
if (full && pullOk) {
|
||||
offlineOutOfScope = markOutOfScopeOffline(syncedCodes);
|
||||
if (StrUtil.isBlank(nextSince)) {
|
||||
nextSince = LocalDateTime.now().format(DT);
|
||||
}
|
||||
}
|
||||
cursor.setUpdatedSince(nextSince);
|
||||
cursor.setLastSyncAt(LocalDateTime.now());
|
||||
cursor.setLastMsg((full ? "full," : "incr,") + "upsert=" + upsert + ",detail=" + detailOk
|
||||
+ ",dailyOk=" + dailyOk + ",dailyRows=" + dailyRows + ",dailyFail=" + dailyFail
|
||||
+ ",offlineOut=" + offlineOutOfScope);
|
||||
cursorMapper.updateById(cursor);
|
||||
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("full", full);
|
||||
out.put("upsert", upsert);
|
||||
out.put("detailOk", detailOk);
|
||||
out.put("dailyOk", Integer.valueOf(dailyOk));
|
||||
out.put("dailyRows", Integer.valueOf(dailyRows));
|
||||
out.put("dailyFail", Integer.valueOf(dailyFail));
|
||||
out.put("offlineOutOfScope", offlineOutOfScope);
|
||||
out.put("updatedSince", nextSince);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 全量同步后,不在本次授权结果集中的本地酒店下架 */
|
||||
private int markOutOfScopeOffline(Set<String> syncedCodes) {
|
||||
List<HotelZaxDO> locals = hotelZaxMapper.selectList(new LambdaQueryWrapperX<>());
|
||||
int n = 0;
|
||||
for (HotelZaxDO h : locals) {
|
||||
if (h == null || StrUtil.isBlank(h.getHotelCode())) {
|
||||
continue;
|
||||
}
|
||||
if (syncedCodes.contains(h.getHotelCode().trim())) {
|
||||
continue;
|
||||
}
|
||||
if ("OFFLINE".equalsIgnoreCase(h.getStatus())) {
|
||||
continue;
|
||||
}
|
||||
h.setStatus("OFFLINE");
|
||||
h.setSyncedAt(LocalDateTime.now());
|
||||
hotelZaxMapper.updateById(h);
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> syncHotelByCode(String hotelCode) {
|
||||
if (StrUtil.isBlank(hotelCode)) {
|
||||
throw new IllegalArgumentException("hotelCode 不能为空");
|
||||
}
|
||||
String code = hotelCode.trim();
|
||||
boolean ok = pullDetailResult(code).ok;
|
||||
if (!ok) {
|
||||
throw new IllegalStateException("供应方无此酒店详情: " + code);
|
||||
}
|
||||
Map<String, Object> out = new LinkedHashMap<>(4);
|
||||
out.put("hotelCode", code);
|
||||
out.put("ok", true);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markOffline(String hotelCode) {
|
||||
if (StrUtil.isBlank(hotelCode)) {
|
||||
return;
|
||||
}
|
||||
HotelZaxDO exist = hotelZaxMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getHotelCode, hotelCode.trim())
|
||||
.last("limit 1"));
|
||||
if (exist == null) {
|
||||
return;
|
||||
}
|
||||
exist.setStatus("OFFLINE");
|
||||
exist.setSyncedAt(LocalDateTime.now());
|
||||
hotelZaxMapper.updateById(exist);
|
||||
}
|
||||
|
||||
private void upsertHotelBrief(JSONObject row) {
|
||||
String code = row.getStr("hotelCode");
|
||||
HotelZaxDO exist = hotelZaxMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getHotelCode, code)
|
||||
.last("limit 1"));
|
||||
HotelZaxDO entity = exist == null ? new HotelZaxDO() : exist;
|
||||
entity.setHotelCode(code);
|
||||
entity.setHotelName(row.getStr("hotelName"));
|
||||
entity.setGroupCode(row.getStr("groupCode"));
|
||||
entity.setCoverImage(row.getStr("coverImage"));
|
||||
entity.setAddress(row.getStr("address"));
|
||||
entity.setDistrict(row.getStr("district"));
|
||||
entity.setDistrictAdcode(row.getLong("districtAdcode"));
|
||||
entity.setBizArea(cleanJunk(row.getStr("bizArea")));
|
||||
entity.setStarLevel(row.getInt("starLevel"));
|
||||
entity.setScore(row.getBigDecimal("score"));
|
||||
entity.setFacilityTags(row.getStr("facilityTags"));
|
||||
entity.setLongitude(row.getBigDecimal("longitude"));
|
||||
entity.setLatitude(row.getBigDecimal("latitude"));
|
||||
if (StrUtil.isNotBlank(row.getStr("phone"))) {
|
||||
entity.setPhone(row.getStr("phone"));
|
||||
}
|
||||
BigDecimal minSell = row.getBigDecimal("minSellPrice");
|
||||
if (minSell != null) {
|
||||
entity.setMinSellPrice(minSell);
|
||||
}
|
||||
entity.setStatus(StrUtil.blankToDefault(row.getStr("status"), "ACTIVE"));
|
||||
entity.setSupplierUpdateTime(parseDt(row.getStr("updateTime")));
|
||||
entity.setSyncedAt(LocalDateTime.now());
|
||||
if (exist == null) {
|
||||
hotelZaxMapper.insert(entity);
|
||||
}
|
||||
else {
|
||||
hotelZaxMapper.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private DetailPullResult pullDetailResult(String hotelCode) {
|
||||
DetailPullResult result = new DetailPullResult();
|
||||
Map<String, Object> req = new HashMap<>();
|
||||
req.put("hotelCode", hotelCode);
|
||||
JSONObject resp = openClient.post("hotel/detail", req);
|
||||
JSONObject data = resp == null ? null : resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
return result;
|
||||
}
|
||||
HotelZaxDO exist = hotelZaxMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getHotelCode, hotelCode)
|
||||
.last("limit 1"));
|
||||
if (exist == null) {
|
||||
upsertHotelBrief(data);
|
||||
exist = hotelZaxMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDO>()
|
||||
.eq(HotelZaxDO::getHotelCode, hotelCode)
|
||||
.last("limit 1"));
|
||||
if (exist == null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
exist.setIntro(data.getStr("intro"));
|
||||
exist.setCarouselImages(data.getStr("carouselImages"));
|
||||
if (StrUtil.isNotBlank(data.getStr("phone"))) {
|
||||
exist.setPhone(data.getStr("phone"));
|
||||
}
|
||||
Object rooms = data.get("rooms");
|
||||
Object products = data.get("products");
|
||||
exist.setRoomsJson(rooms == null ? null : JSONUtil.toJsonStr(rooms));
|
||||
exist.setProductsJson(products == null ? null : JSONUtil.toJsonStr(products));
|
||||
if (data.getBigDecimal("minSellPrice") != null) {
|
||||
exist.setMinSellPrice(data.getBigDecimal("minSellPrice"));
|
||||
}
|
||||
if (StrUtil.isNotBlank(data.getStr("coverImage"))) {
|
||||
exist.setCoverImage(data.getStr("coverImage"));
|
||||
}
|
||||
if (StrUtil.isNotBlank(data.getStr("hotelName"))) {
|
||||
exist.setHotelName(data.getStr("hotelName"));
|
||||
}
|
||||
if (StrUtil.isNotBlank(data.getStr("status"))) {
|
||||
exist.setStatus(data.getStr("status"));
|
||||
}
|
||||
exist.setSyncedAt(LocalDateTime.now());
|
||||
hotelZaxMapper.updateById(exist);
|
||||
|
||||
replaceRooms(exist, data.getJSONArray("rooms"));
|
||||
replaceProducts(exist, data.getJSONArray("products"));
|
||||
result.ok = true;
|
||||
try {
|
||||
result.dailyRows = pullDailyWithRetry(exist);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result.dailyFail = true;
|
||||
log.warn("|hotel-zax-sync|daily失败|code={}|{}", hotelCode, e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static class DetailPullResult {
|
||||
private boolean ok;
|
||||
private int dailyRows;
|
||||
private boolean dailyFail;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> syncDailyByHotelId(Long hotelId) {
|
||||
if (hotelId == null) {
|
||||
throw new IllegalArgumentException("hotelId 不能为空");
|
||||
}
|
||||
HotelZaxDO hotel = hotelZaxMapper.selectById(hotelId);
|
||||
if (hotel == null || StrUtil.isBlank(hotel.getHotelCode())) {
|
||||
throw new IllegalStateException("本地酒店不存在");
|
||||
}
|
||||
int upsert = pullDailyWithRetry(hotel);
|
||||
Map<String, Object> out = new LinkedHashMap<String, Object>(4);
|
||||
out.put("hotelId", hotelId);
|
||||
out.put("hotelCode", hotel.getHotelCode());
|
||||
out.put("upsert", Integer.valueOf(upsert));
|
||||
return out;
|
||||
}
|
||||
|
||||
private int pullDailyWithRetry(HotelZaxDO hotel) {
|
||||
Exception last = null;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
try {
|
||||
return pullDaily(hotel);
|
||||
}
|
||||
catch (Exception e) {
|
||||
last = e;
|
||||
String msg = e.getMessage() == null ? "" : e.getMessage();
|
||||
if ((msg.contains("频繁") || msg.contains("QPS") || msg.contains("限流")) && i < 3) {
|
||||
sleepQuiet(400L * (i + 1));
|
||||
continue;
|
||||
}
|
||||
throw e instanceof RuntimeException ? (RuntimeException) e : new IllegalStateException(msg, e);
|
||||
}
|
||||
}
|
||||
if (last instanceof RuntimeException) {
|
||||
throw (RuntimeException) last;
|
||||
}
|
||||
throw new IllegalStateException(last == null ? "拉取日历失败" : last.getMessage(), last);
|
||||
}
|
||||
|
||||
/** 拉取资安心 hotel/daily 写入本地日历(窗口默认 90 天)。 */
|
||||
private int pullDaily(HotelZaxDO hotel) {
|
||||
int days = openProperties.getDailySyncDays();
|
||||
if (days <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (days > 90) {
|
||||
days = 90;
|
||||
}
|
||||
LocalDate from = LocalDate.now();
|
||||
LocalDate to = from.plusDays(days - 1);
|
||||
Map<String, Object> req = new HashMap<String, Object>();
|
||||
req.put("hotelCode", hotel.getHotelCode());
|
||||
req.put("startDate", from.toString());
|
||||
req.put("endDate", to.toString());
|
||||
JSONObject resp = openClient.post("hotel/daily", req);
|
||||
JSONObject data = resp == null ? null : resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
return 0;
|
||||
}
|
||||
JSONArray list = data.getJSONArray("list");
|
||||
if (list == null || list.isEmpty()) {
|
||||
log.warn("|hotel-zax-sync|daily空|code={}|from={}|to={}|msg=供应方该窗口无日历,请先在资安心维护价态|",
|
||||
hotel.getHotelCode(), from, to);
|
||||
return 0;
|
||||
}
|
||||
List<HotelZaxProductDO> products = productMapper.selectList(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelId, hotel.getId()));
|
||||
Map<String, HotelZaxProductDO> productMap = new HashMap<String, HotelZaxProductDO>();
|
||||
if (products != null) {
|
||||
for (HotelZaxProductDO p : products) {
|
||||
if (p != null && StrUtil.isNotBlank(p.getProductCode())) {
|
||||
productMap.put(p.getProductCode().trim(), p);
|
||||
}
|
||||
}
|
||||
}
|
||||
int upsert = 0;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
JSONObject row = list.getJSONObject(i);
|
||||
if (row == null || StrUtil.isBlank(row.getStr("productCode")) || StrUtil.isBlank(row.getStr("stayDate"))) {
|
||||
continue;
|
||||
}
|
||||
String productCode = row.getStr("productCode").trim();
|
||||
LocalDate stayDate = parseDay(row.get("stayDate") == null ? null : String.valueOf(row.get("stayDate")));
|
||||
if (stayDate == null) {
|
||||
continue;
|
||||
}
|
||||
HotelZaxProductDO product = productMap.get(productCode);
|
||||
HotelZaxDailyDO exist = dailyMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDailyDO>()
|
||||
.eq(HotelZaxDailyDO::getHotelId, hotel.getId())
|
||||
.eq(HotelZaxDailyDO::getProductCode, productCode)
|
||||
.eq(HotelZaxDailyDO::getStayDate, stayDate)
|
||||
.last("limit 1"));
|
||||
HotelZaxDailyDO entity = exist == null ? new HotelZaxDailyDO() : exist;
|
||||
entity.setHotelId(hotel.getId());
|
||||
entity.setHotelCode(hotel.getHotelCode());
|
||||
entity.setProductCode(productCode);
|
||||
if (product != null) {
|
||||
entity.setProductId(product.getId());
|
||||
entity.setRoomTypeCode(product.getRoomTypeCode());
|
||||
}
|
||||
entity.setStayDate(stayDate);
|
||||
if (row.getBigDecimal("costPrice") != null) {
|
||||
entity.setCostPrice(row.getBigDecimal("costPrice"));
|
||||
}
|
||||
entity.setSellPrice(row.getBigDecimal("sellPrice"));
|
||||
entity.setStock(row.getInt("stock"));
|
||||
Integer status = row.getInt("dailyStatus");
|
||||
entity.setDailyStatus(status == null ? Integer.valueOf(1) : status);
|
||||
if (exist == null) {
|
||||
Integer sold = row.getInt("sold");
|
||||
entity.setSold(sold == null ? Integer.valueOf(0) : sold);
|
||||
dailyMapper.insert(entity);
|
||||
}
|
||||
else {
|
||||
dailyMapper.updateById(entity);
|
||||
}
|
||||
upsert++;
|
||||
}
|
||||
return upsert;
|
||||
}
|
||||
|
||||
private static LocalDate parseDay(String raw) {
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String s = raw.trim().replace('/', '-');
|
||||
if (s.length() >= 10) {
|
||||
s = s.substring(0, 10);
|
||||
}
|
||||
return LocalDate.parse(s);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 全量替换该酒店房型(与供应侧一对多一致) */
|
||||
private void replaceRooms(HotelZaxDO hotel, JSONArray rooms) {
|
||||
List<HotelZaxRoomDO> old = roomMapper.selectList(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.eq(HotelZaxRoomDO::getHotelCode, hotel.getHotelCode()));
|
||||
Set<String> keep = new HashSet<>();
|
||||
if (rooms != null) {
|
||||
for (int i = 0; i < rooms.size(); i++) {
|
||||
JSONObject r = rooms.getJSONObject(i);
|
||||
if (r == null || StrUtil.isBlank(r.getStr("roomTypeCode"))) {
|
||||
continue;
|
||||
}
|
||||
String rtc = r.getStr("roomTypeCode").trim();
|
||||
keep.add(rtc);
|
||||
HotelZaxRoomDO exist = roomMapper.selectOne(new LambdaQueryWrapperX<HotelZaxRoomDO>()
|
||||
.eq(HotelZaxRoomDO::getHotelCode, hotel.getHotelCode())
|
||||
.eq(HotelZaxRoomDO::getRoomTypeCode, rtc)
|
||||
.last("limit 1"));
|
||||
HotelZaxRoomDO row = exist == null ? new HotelZaxRoomDO() : exist;
|
||||
row.setHotelId(hotel.getId());
|
||||
row.setHotelCode(hotel.getHotelCode());
|
||||
row.setGroupCode(hotel.getGroupCode());
|
||||
row.setRoomTypeCode(rtc);
|
||||
row.setRoomTypeName(r.getStr("roomTypeName"));
|
||||
row.setBedType(r.getStr("bedType"));
|
||||
row.setCapacity(r.getInt("capacity"));
|
||||
row.setRoomNumber(r.getInt("roomNumber"));
|
||||
row.setRawJson(JSONUtil.toJsonStr(r));
|
||||
if (exist == null) {
|
||||
roomMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
roomMapper.updateById(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (HotelZaxRoomDO o : old) {
|
||||
if (o != null && !keep.contains(o.getRoomTypeCode())) {
|
||||
roomMapper.deleteById(o.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void replaceProducts(HotelZaxDO hotel, JSONArray products) {
|
||||
List<HotelZaxProductDO> old = productMapper.selectList(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelCode, hotel.getHotelCode()));
|
||||
Set<String> keep = new HashSet<>();
|
||||
if (products != null) {
|
||||
for (int i = 0; i < products.size(); i++) {
|
||||
JSONObject p = products.getJSONObject(i);
|
||||
if (p == null || StrUtil.isBlank(p.getStr("productCode"))) {
|
||||
continue;
|
||||
}
|
||||
String pc = p.getStr("productCode").trim();
|
||||
keep.add(pc);
|
||||
HotelZaxProductDO exist = productMapper.selectOne(new LambdaQueryWrapperX<HotelZaxProductDO>()
|
||||
.eq(HotelZaxProductDO::getHotelCode, hotel.getHotelCode())
|
||||
.eq(HotelZaxProductDO::getProductCode, pc)
|
||||
.last("limit 1"));
|
||||
HotelZaxProductDO row = exist == null ? new HotelZaxProductDO() : exist;
|
||||
row.setHotelId(hotel.getId());
|
||||
row.setHotelCode(hotel.getHotelCode());
|
||||
row.setGroupCode(hotel.getGroupCode());
|
||||
row.setRoomTypeCode(p.getStr("roomTypeCode"));
|
||||
row.setProductCode(pc);
|
||||
row.setProductName(p.getStr("productName"));
|
||||
row.setPriceCode(p.getStr("priceCode"));
|
||||
row.setCancelCode(p.getStr("cancelCode"));
|
||||
row.setMealType(p.getInt("mealType"));
|
||||
row.setMealCount(p.getInt("mealCount"));
|
||||
row.setSellPrice(p.getBigDecimal("sellPrice"));
|
||||
row.setStock(p.getInt("stock"));
|
||||
row.setRawJson(JSONUtil.toJsonStr(p));
|
||||
if (exist == null) {
|
||||
productMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
productMapper.updateById(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (HotelZaxProductDO o : old) {
|
||||
if (o != null && !keep.contains(o.getProductCode())) {
|
||||
dailyMapper.delete(new LambdaQueryWrapperX<HotelZaxDailyDO>()
|
||||
.eq(HotelZaxDailyDO::getHotelId, hotel.getId())
|
||||
.eq(HotelZaxDailyDO::getProductCode, o.getProductCode()));
|
||||
productMapper.deleteById(o.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HotelZaxSyncCursorDO getOrCreateCursor() {
|
||||
HotelZaxSyncCursorDO c = cursorMapper.selectOne(new LambdaQueryWrapperX<HotelZaxSyncCursorDO>()
|
||||
.eq(HotelZaxSyncCursorDO::getCursorKey, CURSOR_KEY)
|
||||
.last("limit 1"));
|
||||
if (c != null) {
|
||||
return c;
|
||||
}
|
||||
c = new HotelZaxSyncCursorDO();
|
||||
c.setCursorKey(CURSOR_KEY);
|
||||
c.setUpdatedSince(null);
|
||||
cursorMapper.insert(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
private static String cleanJunk(String v) {
|
||||
if (StrUtil.isBlank(v)) {
|
||||
return null;
|
||||
}
|
||||
String s = v.trim();
|
||||
if ("[]".equals(s) || "{}".equals(s) || "()".equals(s)) {
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static void sleepQuiet(long ms) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalDateTime parseDt(String s) {
|
||||
if (StrUtil.isBlank(s)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDateTime.parse(s.trim(), DT);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 小如酒店:供应侧下单 + 本地发起微信支付。
|
||||
*/
|
||||
public interface HotelZaxTradeService {
|
||||
|
||||
/**
|
||||
* 可订校验 → 落本地待支付单 → 创建微信支付单。
|
||||
* 供应方 order/create 只在支付成功回调里调用,避免未付款就预定成功。
|
||||
*/
|
||||
Map<String, Object> createOrderAndPay(Long userId, Map<String, Object> body);
|
||||
|
||||
/** 支付回调:标记已支付,再向资安心预定 */
|
||||
boolean updatePaidIfMatch(Long orderId, Long payOrderId);
|
||||
|
||||
/**
|
||||
* 支付成功通知。商户单号可能与商城订单撞号,先按 payOrderId 认酒店单。
|
||||
*
|
||||
* @return true=酒店单已处理
|
||||
*/
|
||||
boolean handlePayNotify(String merchantOrderId, Long payOrderId);
|
||||
|
||||
/** 按支付单查询本地订单详情(供小程序支付结果页) */
|
||||
Map<String, Object> getByPayOrderId(Long userId, Long payOrderId);
|
||||
|
||||
/** 按渠道单号查询 */
|
||||
Map<String, Object> getByChannelNo(Long userId, String channelNo);
|
||||
|
||||
/** 当前用户酒店订单分页 */
|
||||
PageResult<Map<String, Object>> pageMyOrders(Long userId, Integer pageNo, Integer pageSize,
|
||||
String orderStatus, Boolean payStatus);
|
||||
|
||||
default PageResult<Map<String, Object>> pageMyOrders(Long userId, Integer pageNo, Integer pageSize) {
|
||||
return pageMyOrders(userId, pageNo, pageSize, null, null);
|
||||
}
|
||||
|
||||
/** 超时未支付自动取消,返回取消条数 */
|
||||
int cancelUnpaidTimeout();
|
||||
|
||||
/**
|
||||
* 后台/用户取消:已支付则原路退款;已向资安心预定则先调供应方取消。
|
||||
*
|
||||
* @param notifySupplier true=主动通知资安心取消;资安心回调进来时应为 false,避免循环
|
||||
*/
|
||||
void cancelOrder(Long orderId, String reason, boolean notifySupplier);
|
||||
|
||||
/** 用户按渠道单号取消(校验归属) */
|
||||
void cancelByChannelNo(Long userId, String channelNo, String reason);
|
||||
|
||||
/**
|
||||
* 资安心订单状态回调:更新本地状态;若已取消且已支付则退款给用户。
|
||||
*/
|
||||
Map<String, Object> applySupplierStatusNotify(Map<String, Object> body);
|
||||
}
|
||||
+856
@@ -0,0 +1,856 @@
|
||||
package cn.iocoder.yudao.module.product.service.hotel;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.module.pay.api.order.PayOrderApi;
|
||||
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
|
||||
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.product.dal.dataobject.hotel.HotelZaxOrderDO;
|
||||
import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxOrderMapper;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenClient;
|
||||
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.REFUND_EXISTS;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.REFUND_HAS_REFUNDING;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_CANCEL_FAIL;
|
||||
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_ORDER_NOT_EXISTS;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private static final Duration PAY_EXPIRE = Duration.ofMinutes(10);
|
||||
|
||||
/** 支付商户单号前缀,避免与 trade_order.id 撞号 */
|
||||
private static final String PAY_MERCHANT_PREFIX = "HZ";
|
||||
|
||||
private final ZaxHotelOpenClient openClient;
|
||||
|
||||
private final ZaxHotelOpenProperties openProperties;
|
||||
|
||||
private final HotelZaxOrderMapper orderMapper;
|
||||
|
||||
private final HotelZaxLocalService hotelZaxLocalService;
|
||||
|
||||
private final PayOrderApi payOrderApi;
|
||||
|
||||
private final PayRefundApi payRefundApi;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> createOrderAndPay(Long userId, Map<String, Object> body) {
|
||||
if (userId == null) {
|
||||
throw invalidParamException("请先登录");
|
||||
}
|
||||
Map<String, Object> req = body == null ? new HashMap<>() : new HashMap<>(body);
|
||||
String hotelCode = trim(req.get("hotelCode"));
|
||||
String productCode = trim(req.get("productCode"));
|
||||
String roomTypeCode = trim(req.get("roomTypeCode"));
|
||||
LocalDate checkIn = parseDay(trim(req.get("checkInDate")));
|
||||
LocalDate checkOut = parseDay(trim(req.get("checkOutDate")));
|
||||
int roomCount = Math.max(1, toInt(req.get("count"), 1));
|
||||
List<Map<String, Object>> guestList = parseGuests(req.get("guestList"));
|
||||
if (StrUtil.hasBlank(hotelCode, productCode) || checkIn == null || checkOut == null) {
|
||||
throw invalidParamException("酒店/产品/入住离店日期不能为空");
|
||||
}
|
||||
if (!checkOut.isAfter(checkIn)) {
|
||||
throw invalidParamException("离店日期必须晚于入住日期");
|
||||
}
|
||||
if (CollUtil.isEmpty(guestList)) {
|
||||
throw invalidParamException("请至少填写一位入住人(姓名+手机号)");
|
||||
}
|
||||
// 联系人兼容供应方:默认取首位入住人;若单独传了 contact* 则优先用 contact*
|
||||
String contactName = trim(req.get("contactName"));
|
||||
String contactPhone = trim(req.get("contactPhone"));
|
||||
if (StrUtil.isBlank(contactName)) {
|
||||
contactName = trim(guestList.get(0).get("name"));
|
||||
}
|
||||
if (StrUtil.isBlank(contactPhone)) {
|
||||
contactPhone = trim(guestList.get(0).get("mobile"));
|
||||
}
|
||||
if (StrUtil.hasBlank(contactName, contactPhone)) {
|
||||
throw invalidParamException("请填写入住人姓名与手机号");
|
||||
}
|
||||
if (!contactPhone.matches("^1\\d{10}$")) {
|
||||
throw invalidParamException("入住人手机号格式不正确");
|
||||
}
|
||||
BigDecimal totalAmount = quoteStayAmount(hotelCode, productCode, checkIn, checkOut, roomCount);
|
||||
if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw invalidParamException("所选日期无可用日历价态,请更换日期或联系商家维护日历");
|
||||
}
|
||||
int nights = (int) (checkOut.toEpochDay() - checkIn.toEpochDay());
|
||||
String channelNo = trim(req.get("channelNo"));
|
||||
if (StrUtil.isBlank(channelNo)) {
|
||||
channelNo = "XR" + userId + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
Map<String, Object> openBody = new LinkedHashMap<>();
|
||||
openBody.put("channelNo", channelNo);
|
||||
openBody.put("hotelCode", hotelCode);
|
||||
openBody.put("productCode", productCode);
|
||||
openBody.put("roomTypeCode", roomTypeCode);
|
||||
openBody.put("checkInDate", checkIn.format(DAY));
|
||||
openBody.put("checkOutDate", checkOut.format(DAY));
|
||||
openBody.put("count", roomCount);
|
||||
openBody.put("totalAmount", totalAmount);
|
||||
openBody.put("contactName", contactName);
|
||||
openBody.put("contactPhone", contactPhone);
|
||||
openBody.put("guestList", guestList);
|
||||
if (StrUtil.isNotBlank(trim(req.get("groupCode")))) {
|
||||
openBody.put("groupCode", trim(req.get("groupCode")));
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(req.get("priceCode")))) {
|
||||
openBody.put("priceCode", trim(req.get("priceCode")));
|
||||
}
|
||||
|
||||
// 1) 可订校验(不落供应方订单)
|
||||
openClient.post("order/check", openBody);
|
||||
|
||||
// 2) 本地待支付单;供应方预定推迟到微信支付成功
|
||||
HotelZaxOrderDO local = new HotelZaxOrderDO();
|
||||
local.setUserId(userId);
|
||||
local.setChannelNo(channelNo);
|
||||
local.setHotelCode(hotelCode);
|
||||
local.setHotelName(trim(req.get("hotelName")));
|
||||
local.setProductCode(productCode);
|
||||
local.setProductName(trim(req.get("productName")));
|
||||
local.setRoomTypeCode(roomTypeCode);
|
||||
local.setCheckInDate(checkIn);
|
||||
local.setCheckOutDate(checkOut);
|
||||
local.setNights(nights);
|
||||
local.setRoomCount(roomCount);
|
||||
local.setContactName(contactName);
|
||||
local.setContactPhone(contactPhone);
|
||||
local.setGuestJson(JSONUtil.toJsonStr(guestList));
|
||||
local.setSellAmount(totalAmount);
|
||||
local.setOrderStatus("UNPAID");
|
||||
local.setPayStatus(false);
|
||||
local.setPayloadJson(JSONUtil.toJsonStr(openBody));
|
||||
orderMapper.insert(local);
|
||||
|
||||
// 3) 支付单(金额:分)
|
||||
int priceFen = totalAmount.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).intValue();
|
||||
if (priceFen <= 0) {
|
||||
throw invalidParamException("支付金额必须大于 0");
|
||||
}
|
||||
String subject = StrUtil.maxLength(StrUtil.blankToDefault(local.getHotelName(), "酒店预订"), 32);
|
||||
Long payOrderId;
|
||||
try {
|
||||
payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
|
||||
.setAppKey(StrUtil.blankToDefault(openProperties.getPayAppKey(), "mall"))
|
||||
.setUserIp(StrUtil.blankToDefault(getClientIP(), "127.0.0.1"))
|
||||
.setMerchantOrderId(PAY_MERCHANT_PREFIX + local.getId())
|
||||
.setSubject(subject)
|
||||
.setBody(StrUtil.maxLength(StrUtil.blankToDefault(local.getProductName(), "酒店订单"), 128))
|
||||
.setPrice(priceFen)
|
||||
.setExpireTime(addTime(PAY_EXPIRE)));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("|hotel-pay|创建支付单失败|channelNo={}|{}", channelNo, e.getMessage(), e);
|
||||
throw invalidParamException("创建支付单失败:{}(请确认支付应用 mall 已配置,且已执行 hotel_zax_order_pay.sql)",
|
||||
StrUtil.maxLength(StrUtil.blankToDefault(e.getMessage(), e.getClass().getSimpleName()), 80));
|
||||
}
|
||||
local.setPayOrderId(payOrderId);
|
||||
orderMapper.updateById(local);
|
||||
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("id", local.getId());
|
||||
out.put("channelNo", local.getChannelNo());
|
||||
out.put("orderNo", local.getOrderNo());
|
||||
out.put("platformNo", local.getPlatformNo());
|
||||
out.put("payOrderId", payOrderId);
|
||||
out.put("totalAmount", totalAmount);
|
||||
out.put("nights", nights);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handlePayNotify(String merchantOrderId, Long payOrderId) {
|
||||
if (payOrderId != null) {
|
||||
HotelZaxOrderDO byPay = orderMapper.selectOne(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getPayOrderId, payOrderId)
|
||||
.last("limit 1"));
|
||||
if (byPay != null) {
|
||||
return updatePaidIfMatch(byPay.getId(), payOrderId);
|
||||
}
|
||||
}
|
||||
Long hotelId = parseHotelMerchantOrderId(merchantOrderId);
|
||||
if (hotelId == null) {
|
||||
return false;
|
||||
}
|
||||
return updatePaidIfMatch(hotelId, payOrderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updatePaidIfMatch(Long orderId, Long payOrderId) {
|
||||
if (orderId == null || payOrderId == null) {
|
||||
return false;
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
return false;
|
||||
}
|
||||
if (isTerminalUnbookable(order.getOrderStatus())) {
|
||||
log.warn("|hotel-pay|订单已取消/结束,忽略支付成功回调|orderId={}|payOrderId={}|status={}|",
|
||||
orderId, payOrderId, order.getOrderStatus());
|
||||
return true;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
if (order.getPayOrderId() == null || !order.getPayOrderId().equals(payOrderId)) {
|
||||
log.warn("|hotel-pay|支付单不匹配|orderId={}|expect={}|actual={}|", orderId, order.getPayOrderId(),
|
||||
payOrderId);
|
||||
return false;
|
||||
}
|
||||
PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId);
|
||||
if (payOrder == null || !PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
|
||||
log.warn("|hotel-pay|支付单未成功|orderId={}|payOrderId={}|status={}|", orderId, payOrderId,
|
||||
payOrder == null ? null : payOrder.getStatus());
|
||||
return false;
|
||||
}
|
||||
order.setPayStatus(true);
|
||||
order.setPayTime(LocalDateTime.now());
|
||||
if (StrUtil.isBlank(order.getOrderStatus()) || "UNPAID".equalsIgnoreCase(order.getOrderStatus())) {
|
||||
order.setOrderStatus("PAID");
|
||||
}
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
bookSupplierAfterPaid(order);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getByPayOrderId(Long userId, Long payOrderId) {
|
||||
if (payOrderId == null) {
|
||||
return null;
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectOne(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getPayOrderId, payOrderId)
|
||||
.eqIfPresent(HotelZaxOrderDO::getUserId, userId)
|
||||
.last("limit 1"));
|
||||
syncPaidFromPayOrder(order);
|
||||
if (order != null && order.getId() != null) {
|
||||
order = orderMapper.selectById(order.getId());
|
||||
}
|
||||
return toView(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getByChannelNo(Long userId, String channelNo) {
|
||||
if (StrUtil.isBlank(channelNo)) {
|
||||
return null;
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectOne(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getChannelNo, channelNo.trim())
|
||||
.eqIfPresent(HotelZaxOrderDO::getUserId, userId)
|
||||
.last("limit 1"));
|
||||
return toView(order);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<Map<String, Object>> pageMyOrders(Long userId, Integer pageNo, Integer pageSize,
|
||||
String orderStatus, Boolean payStatus) {
|
||||
if (userId == null) {
|
||||
throw invalidParamException("请先登录");
|
||||
}
|
||||
PageParam pageParam = new PageParam();
|
||||
pageParam.setPageNo(pageNo == null || pageNo < 1 ? 1 : pageNo);
|
||||
pageParam.setPageSize(pageSize == null || pageSize < 1 ? 10 : pageSize);
|
||||
PageResult<HotelZaxOrderDO> page = orderMapper.selectPage(pageParam,
|
||||
new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getUserId, userId)
|
||||
.eqIfPresent(HotelZaxOrderDO::getOrderStatus, StrUtil.trim(orderStatus))
|
||||
.eqIfPresent(HotelZaxOrderDO::getPayStatus, payStatus)
|
||||
.orderByDesc(HotelZaxOrderDO::getId));
|
||||
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
|
||||
if (page.getList() != null) {
|
||||
for (HotelZaxOrderDO row : page.getList()) {
|
||||
syncPaidFromPayOrder(row);
|
||||
HotelZaxOrderDO latest = row.getId() == null ? row : orderMapper.selectById(row.getId());
|
||||
list.add(toView(latest == null ? row : latest));
|
||||
}
|
||||
}
|
||||
return new PageResult<Map<String, Object>>(list, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int cancelUnpaidTimeout() {
|
||||
LocalDateTime expireBefore = LocalDateTime.now().minus(PAY_EXPIRE);
|
||||
List<HotelZaxOrderDO> list = orderMapper.selectList(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getOrderStatus, "UNPAID")
|
||||
.and(w -> w.eq(HotelZaxOrderDO::getPayStatus, false).or().isNull(HotelZaxOrderDO::getPayStatus))
|
||||
.lt(HotelZaxOrderDO::getCreateTime, expireBefore)
|
||||
.last("limit 200"));
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return 0;
|
||||
}
|
||||
int n = 0;
|
||||
for (HotelZaxOrderDO order : list) {
|
||||
if (Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
continue;
|
||||
}
|
||||
order.setOrderStatus("CANCELLED");
|
||||
order.setFailMsg("超时未支付自动取消");
|
||||
orderMapper.updateById(order);
|
||||
n++;
|
||||
}
|
||||
if (n > 0) {
|
||||
log.info("|hotel-pay|超时未支付自动取消|count={}|", n);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelOrder(Long orderId, String reason, boolean notifySupplier) {
|
||||
if (orderId == null) {
|
||||
throw invalidParamException("订单 id 不能为空");
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
throw exception(HOTEL_ZAX_ORDER_NOT_EXISTS);
|
||||
}
|
||||
doCancel(order, StrUtil.blankToDefault(reason, "后台取消"), notifySupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelByChannelNo(Long userId, String channelNo, String reason) {
|
||||
if (StrUtil.isBlank(channelNo)) {
|
||||
throw invalidParamException("channelNo 不能为空");
|
||||
}
|
||||
HotelZaxOrderDO order = orderMapper.selectOne(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getChannelNo, channelNo.trim())
|
||||
.eqIfPresent(HotelZaxOrderDO::getUserId, userId)
|
||||
.last("limit 1"));
|
||||
if (order == null) {
|
||||
throw exception(HOTEL_ZAX_ORDER_NOT_EXISTS);
|
||||
}
|
||||
doCancel(order, StrUtil.blankToDefault(reason, "用户取消"), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> applySupplierStatusNotify(Map<String, Object> body) {
|
||||
Map<String, Object> src = body == null ? new HashMap<String, Object>() : body;
|
||||
String channelNo = trim(src.get("channelNo"));
|
||||
if (StrUtil.isBlank(channelNo)) {
|
||||
throw invalidParamException("channelNo 不能为空");
|
||||
}
|
||||
HotelZaxOrderDO exist = orderMapper.selectOne(new LambdaQueryWrapperX<HotelZaxOrderDO>()
|
||||
.eq(HotelZaxOrderDO::getChannelNo, channelNo)
|
||||
.last("limit 1"));
|
||||
HotelZaxOrderDO row = exist == null ? new HotelZaxOrderDO() : exist;
|
||||
row.setChannelNo(channelNo);
|
||||
if (StrUtil.isNotBlank(trim(src.get("orderNo")))) {
|
||||
row.setOrderNo(trim(src.get("orderNo")));
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("platformNo")))) {
|
||||
row.setPlatformNo(trim(src.get("platformNo")));
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("orderStatus")))) {
|
||||
row.setOrderStatus(trim(src.get("orderStatus")));
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("bookStatus")))) {
|
||||
row.setBookStatus(trim(src.get("bookStatus")));
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("hotelCode")))) {
|
||||
row.setHotelCode(trim(src.get("hotelCode")));
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("productCode")))) {
|
||||
row.setProductCode(trim(src.get("productCode")));
|
||||
}
|
||||
if (src.get("sellAmount") != null) {
|
||||
try {
|
||||
row.setSellAmount(new BigDecimal(String.valueOf(src.get("sellAmount"))));
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
// 保留原金额
|
||||
}
|
||||
}
|
||||
if (StrUtil.isNotBlank(trim(src.get("failMsg")))) {
|
||||
row.setFailMsg(trim(src.get("failMsg")));
|
||||
}
|
||||
row.setLastNotifyAt(LocalDateTime.now());
|
||||
if (exist == null) {
|
||||
orderMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
orderMapper.updateById(row);
|
||||
}
|
||||
if (isTerminalUnbookable(row.getOrderStatus()) && Boolean.TRUE.equals(row.getPayStatus())) {
|
||||
refundIfPaid(row, StrUtil.blankToDefault(trim(src.get("failMsg")), "供应方取消退款"));
|
||||
row.setOrderStatus("CANCELLED");
|
||||
orderMapper.updateById(row);
|
||||
}
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("channelNo", channelNo);
|
||||
out.put("orderStatus", row.getOrderStatus());
|
||||
out.put("id", row.getId());
|
||||
return out;
|
||||
}
|
||||
|
||||
private void doCancel(HotelZaxOrderDO order, String reason, boolean notifySupplier) {
|
||||
boolean alreadyClosed = isTerminalUnbookable(order.getOrderStatus());
|
||||
if (alreadyClosed && (!Boolean.TRUE.equals(order.getPayStatus()) || isLocalRefunded(order))) {
|
||||
return;
|
||||
}
|
||||
if (!alreadyClosed && notifySupplier && shouldNotifySupplierCancel(order)) {
|
||||
try {
|
||||
Map<String, Object> req = new HashMap<String, Object>();
|
||||
req.put("channelNo", order.getChannelNo());
|
||||
openClient.post("order/cancel", req);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-cancel|通知资安心取消失败|channelNo={}|{}", order.getChannelNo(), e.getMessage());
|
||||
throw exception(HOTEL_ZAX_ORDER_CANCEL_FAIL, StrUtil.maxLength(
|
||||
StrUtil.blankToDefault(e.getMessage(), "供应方取消失败"), 80));
|
||||
}
|
||||
}
|
||||
if (Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
refundIfPaid(order, reason);
|
||||
}
|
||||
order.setOrderStatus("CANCELLED");
|
||||
order.setFailMsg(StrUtil.maxLength(StrUtil.blankToDefault(reason, "已取消"), 200));
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
|
||||
private static boolean shouldNotifySupplierCancel(HotelZaxOrderDO order) {
|
||||
if (order == null || StrUtil.isBlank(order.getChannelNo())) {
|
||||
return false;
|
||||
}
|
||||
if ("UNPAID".equalsIgnoreCase(StrUtil.trim(order.getOrderStatus()))) {
|
||||
return false;
|
||||
}
|
||||
return StrUtil.isNotBlank(order.getOrderNo());
|
||||
}
|
||||
|
||||
private void refundIfPaid(HotelZaxOrderDO order, String reason) {
|
||||
if (order == null || !Boolean.TRUE.equals(order.getPayStatus()) || order.getPayOrderId() == null) {
|
||||
return;
|
||||
}
|
||||
if (isLocalRefunded(order)) {
|
||||
return;
|
||||
}
|
||||
int priceFen = order.getSellAmount() == null ? 0
|
||||
: order.getSellAmount().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).intValue();
|
||||
if (priceFen <= 0) {
|
||||
markLocalRefunded(order);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
payRefundApi.createRefund(new PayRefundCreateReqDTO()
|
||||
.setAppKey(StrUtil.blankToDefault(openProperties.getPayAppKey(), "mall"))
|
||||
.setUserIp(StrUtil.blankToDefault(getClientIP(), "127.0.0.1"))
|
||||
.setMerchantOrderId(resolvePayMerchantOrderId(order))
|
||||
.setMerchantRefundId("HZ" + order.getId())
|
||||
.setReason(StrUtil.maxLength(StrUtil.blankToDefault(reason, "酒店订单取消退款"), 128))
|
||||
.setPrice(priceFen));
|
||||
}
|
||||
catch (ServiceException e) {
|
||||
Integer code = e.getCode();
|
||||
if (REFUND_EXISTS.getCode().equals(code) || REFUND_HAS_REFUNDING.getCode().equals(code)) {
|
||||
log.info("|hotel-refund|退款单已存在|orderId={}|code={}|", order.getId(), code);
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
markLocalRefunded(order);
|
||||
}
|
||||
|
||||
private static boolean isLocalRefunded(HotelZaxOrderDO order) {
|
||||
if (order == null || StrUtil.isBlank(order.getPayloadJson())) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return Boolean.TRUE.equals(JSONUtil.parseObj(order.getPayloadJson()).getBool("localRefunded"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void markLocalRefunded(HotelZaxOrderDO order) {
|
||||
JSONObject p;
|
||||
try {
|
||||
p = JSONUtil.parseObj(StrUtil.blankToDefault(order.getPayloadJson(), "{}"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
p = new JSONObject();
|
||||
}
|
||||
p.set("localRefunded", true);
|
||||
order.setPayloadJson(p.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付成功后才向资安心下单。channelNo 幂等,失败记 BOOK_FAIL,不回滚已支付。
|
||||
*/
|
||||
private void bookSupplierAfterPaid(HotelZaxOrderDO order) {
|
||||
if (order == null || StrUtil.isNotBlank(order.getOrderNo())) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> openBody = rebuildOpenBody(order);
|
||||
try {
|
||||
JSONObject createResp = openClient.post("order/create", openBody);
|
||||
JSONObject data = createResp == null ? null : createResp.getJSONObject("data");
|
||||
if (data != null) {
|
||||
order.setOrderNo(data.getStr("orderNo"));
|
||||
order.setPlatformNo(data.getStr("platformNo"));
|
||||
if (StrUtil.isNotBlank(data.getStr("orderStatus"))) {
|
||||
order.setOrderStatus(data.getStr("orderStatus"));
|
||||
}
|
||||
else {
|
||||
order.setOrderStatus("CREATED");
|
||||
}
|
||||
order.setBookStatus(data.getStr("bookStatus"));
|
||||
order.setFailMsg(null);
|
||||
order.setPayloadJson(data.toString());
|
||||
}
|
||||
else {
|
||||
order.setOrderStatus("CREATED");
|
||||
}
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("|hotel-pay|支付成功但供应方下单失败|channelNo={}|{}", order.getChannelNo(), e.getMessage(), e);
|
||||
order.setOrderStatus("BOOK_FAIL");
|
||||
order.setFailMsg(StrUtil.maxLength(StrUtil.blankToDefault(e.getMessage(), "供应方下单失败"), 200));
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> rebuildOpenBody(HotelZaxOrderDO order) {
|
||||
if (StrUtil.isNotBlank(order.getPayloadJson())) {
|
||||
try {
|
||||
JSONObject p = JSONUtil.parseObj(order.getPayloadJson());
|
||||
if (StrUtil.isNotBlank(p.getStr("channelNo")) && StrUtil.isNotBlank(p.getStr("hotelCode"))) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
// 回退字段拼装
|
||||
}
|
||||
}
|
||||
Map<String, Object> openBody = new LinkedHashMap<>();
|
||||
openBody.put("channelNo", order.getChannelNo());
|
||||
openBody.put("hotelCode", order.getHotelCode());
|
||||
openBody.put("productCode", order.getProductCode());
|
||||
openBody.put("roomTypeCode", order.getRoomTypeCode());
|
||||
openBody.put("checkInDate", order.getCheckInDate() == null ? null : order.getCheckInDate().format(DAY));
|
||||
openBody.put("checkOutDate", order.getCheckOutDate() == null ? null : order.getCheckOutDate().format(DAY));
|
||||
openBody.put("count", order.getRoomCount() == null ? 1 : order.getRoomCount());
|
||||
openBody.put("totalAmount", order.getSellAmount());
|
||||
openBody.put("contactName", order.getContactName());
|
||||
openBody.put("contactPhone", order.getContactPhone());
|
||||
openBody.put("guestList", JSONUtil.parseArray(StrUtil.blankToDefault(order.getGuestJson(), "[]")));
|
||||
return openBody;
|
||||
}
|
||||
|
||||
private BigDecimal quoteStayAmount(String hotelCode, String productCode, LocalDate checkIn, LocalDate checkOut,
|
||||
int rooms) {
|
||||
BigDecimal local = hotelZaxLocalService.quoteStay(hotelCode, productCode, checkIn, checkOut, rooms);
|
||||
if (local != null) {
|
||||
return local;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> req = new HashMap<String, Object>();
|
||||
req.put("hotelCode", hotelCode);
|
||||
req.put("productCode", productCode);
|
||||
req.put("startDate", checkIn.format(DAY));
|
||||
req.put("endDate", checkOut.minusDays(1).format(DAY));
|
||||
JSONObject resp = openClient.post("hotel/daily", req);
|
||||
JSONObject data = resp == null ? null : resp.getJSONObject("data");
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
JSONArray list = data.getJSONArray("list");
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
JSONObject row = list.getJSONObject(i);
|
||||
if (row != null) {
|
||||
rows.add(row);
|
||||
}
|
||||
}
|
||||
return hotelZaxLocalService.quoteFromDailyRows(rows, productCode, checkIn, checkOut, rooms);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-quote|供应方日历失败|hotel={}|product={}|{}", hotelCode, productCode, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> toView(HotelZaxOrderDO order) {
|
||||
if (order == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("id", order.getId());
|
||||
m.put("channelNo", order.getChannelNo());
|
||||
m.put("orderNo", order.getOrderNo());
|
||||
m.put("platformNo", order.getPlatformNo());
|
||||
m.put("hotelCode", order.getHotelCode());
|
||||
m.put("hotelName", order.getHotelName());
|
||||
m.put("productCode", order.getProductCode());
|
||||
m.put("productName", order.getProductName());
|
||||
m.put("roomTypeCode", order.getRoomTypeCode());
|
||||
m.put("checkInDate", order.getCheckInDate() == null ? null : order.getCheckInDate().format(DAY));
|
||||
m.put("checkOutDate", order.getCheckOutDate() == null ? null : order.getCheckOutDate().format(DAY));
|
||||
m.put("nights", order.getNights());
|
||||
m.put("count", order.getRoomCount());
|
||||
m.put("contactName", order.getContactName());
|
||||
m.put("contactPhone", order.getContactPhone());
|
||||
m.put("guestList", JSONUtil.parseArray(StrUtil.blankToDefault(order.getGuestJson(), "[]")));
|
||||
m.put("totalAmount", order.getSellAmount());
|
||||
m.put("payOrderId", order.getPayOrderId());
|
||||
m.put("payStatus", order.getPayStatus());
|
||||
m.put("payTime", order.getPayTime());
|
||||
m.put("orderStatus", order.getOrderStatus());
|
||||
m.put("status", order.getOrderStatus());
|
||||
m.put("statusName", customerStatusName(order.getOrderStatus(), order.getPayStatus()));
|
||||
m.put("bookStatus", order.getBookStatus());
|
||||
m.put("failMsg", order.getFailMsg());
|
||||
m.put("createTime", order.getCreateTime());
|
||||
boolean canPay = isAwaitingPay(order);
|
||||
m.put("canPay", canPay);
|
||||
if (canPay && order.getCreateTime() != null) {
|
||||
m.put("payExpireTime", order.getCreateTime().plus(PAY_EXPIRE));
|
||||
m.put("expireTime", order.getCreateTime().plus(PAY_EXPIRE));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Map<String, Object>> parseGuests(Object raw) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
if (raw == null) {
|
||||
return list;
|
||||
}
|
||||
if (raw instanceof List) {
|
||||
for (Object o : (List<?>) raw) {
|
||||
if (o instanceof Map) {
|
||||
Map<?, ?> src = (Map<?, ?>) o;
|
||||
String name = trim(src.get("name"));
|
||||
String mobile = trim(src.get("mobile"));
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
mobile = trim(src.get("guestPhone"));
|
||||
}
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
mobile = trim(src.get("phone"));
|
||||
}
|
||||
if (StrUtil.isBlank(name)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> g = new LinkedHashMap<>();
|
||||
g.put("name", name);
|
||||
if (StrUtil.isNotBlank(mobile)) {
|
||||
g.put("mobile", mobile);
|
||||
g.put("guestPhone", mobile);
|
||||
}
|
||||
list.add(g);
|
||||
}
|
||||
else if (o != null && StrUtil.isNotBlank(String.valueOf(o))) {
|
||||
Map<String, Object> g = new LinkedHashMap<>();
|
||||
g.put("name", String.valueOf(o).trim());
|
||||
list.add(g);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
JSONArray arr = JSONUtil.parseArray(JSONUtil.toJsonStr(raw));
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
JSONObject o = arr.getJSONObject(i);
|
||||
if (o == null || StrUtil.isBlank(o.getStr("name"))) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> g = new LinkedHashMap<>();
|
||||
g.put("name", o.getStr("name").trim());
|
||||
String mobile = StrUtil.blankToDefault(o.getStr("mobile"),
|
||||
StrUtil.blankToDefault(o.getStr("guestPhone"), o.getStr("phone")));
|
||||
if (StrUtil.isNotBlank(mobile)) {
|
||||
g.put("mobile", mobile.trim());
|
||||
g.put("guestPhone", mobile.trim());
|
||||
}
|
||||
list.add(g);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static LocalDate parseDay(String s) {
|
||||
if (StrUtil.isBlank(s)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(s.trim(), DAY);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal toMoney(Object o) {
|
||||
if (o == null || StrUtil.isBlank(String.valueOf(o))) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(o).trim()).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int toInt(Object o, int def) {
|
||||
if (o == null) {
|
||||
return def;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(o).trim());
|
||||
}
|
||||
catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(Object o) {
|
||||
return o == null ? null : StrUtil.trim(String.valueOf(o));
|
||||
}
|
||||
|
||||
private void syncPaidFromPayOrder(HotelZaxOrderDO order) {
|
||||
if (order == null || order.getPayOrderId() == null || Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
updatePaidIfMatch(order.getId(), order.getPayOrderId());
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|hotel-pay|同步支付状态失败|orderId={}|payOrderId={}|{}", order.getId(), order.getPayOrderId(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String resolvePayMerchantOrderId(HotelZaxOrderDO order) {
|
||||
if (order.getPayOrderId() != null) {
|
||||
try {
|
||||
PayOrderRespDTO payOrder = payOrderApi.getOrder(order.getPayOrderId());
|
||||
if (payOrder != null && StrUtil.isNotBlank(payOrder.getMerchantOrderId())) {
|
||||
return payOrder.getMerchantOrderId();
|
||||
}
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
// 回退本地约定
|
||||
}
|
||||
}
|
||||
return PAY_MERCHANT_PREFIX + order.getId();
|
||||
}
|
||||
|
||||
private static Long parseHotelMerchantOrderId(String merchantOrderId) {
|
||||
String raw = StrUtil.trim(merchantOrderId);
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
String num = raw;
|
||||
if (StrUtil.startWithIgnoreCase(raw, PAY_MERCHANT_PREFIX)) {
|
||||
num = raw.substring(PAY_MERCHANT_PREFIX.length());
|
||||
}
|
||||
else if (StrUtil.startWithIgnoreCase(raw, "hotel-")) {
|
||||
num = raw.substring("hotel-".length());
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(num);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAwaitingPay(HotelZaxOrderDO order) {
|
||||
if (order == null || Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
return false;
|
||||
}
|
||||
String s = StrUtil.trim(order.getOrderStatus());
|
||||
if (StrUtil.isBlank(s) || "UNPAID".equalsIgnoreCase(s)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isTerminalUnbookable(String orderStatus) {
|
||||
String s = StrUtil.trim(orderStatus);
|
||||
return "CANCELLED".equalsIgnoreCase(s) || "CANCELED".equalsIgnoreCase(s)
|
||||
|| "COMPLETED".equalsIgnoreCase(s) || "BOOK_FAIL".equalsIgnoreCase(s)
|
||||
|| "UPSTREAM_FAIL".equalsIgnoreCase(s);
|
||||
}
|
||||
|
||||
/** 顾客端状态文案,不直接展示内部英文码 */
|
||||
private static String customerStatusName(String orderStatus, Boolean payStatus) {
|
||||
String s = StrUtil.trim(orderStatus);
|
||||
if ("UNPAID".equals(s)) {
|
||||
return "待支付";
|
||||
}
|
||||
if ("PAID".equals(s) || "PENDING".equals(s)) {
|
||||
return "已支付,预定中";
|
||||
}
|
||||
if ("PENDING_CONFIRM".equals(s) || "NEED_MANUAL".equals(s)) {
|
||||
return "预定处理中";
|
||||
}
|
||||
if ("CREATED".equals(s) || "UPSTREAM_OK".equals(s)) {
|
||||
return "预定成功";
|
||||
}
|
||||
if ("CHECKED_IN".equals(s)) {
|
||||
return "已入住";
|
||||
}
|
||||
if ("COMPLETED".equals(s)) {
|
||||
return "已完成";
|
||||
}
|
||||
if ("CANCELLED".equals(s) || "CANCELED".equals(s)) {
|
||||
return "已取消";
|
||||
}
|
||||
if ("BOOK_FAIL".equals(s) || "UPSTREAM_FAIL".equals(s)) {
|
||||
return "预定失败";
|
||||
}
|
||||
if (Boolean.TRUE.equals(payStatus)) {
|
||||
return "已支付";
|
||||
}
|
||||
return "待支付";
|
||||
}
|
||||
}
|
||||
+24
-2
@@ -1,8 +1,10 @@
|
||||
package cn.iocoder.yudao.module.trade.controller.app.order;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
|
||||
import cn.iocoder.yudao.module.product.api.hotel.HotelZaxOrderApi;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.order.vo.*;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO;
|
||||
@@ -36,6 +38,7 @@ import java.util.stream.Collectors;
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
||||
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_NOT_FOUND;
|
||||
|
||||
@Tag(name = "用户 App - 交易订单")
|
||||
@RestController
|
||||
@@ -49,6 +52,8 @@ public class AppTradeOrderController {
|
||||
@Resource
|
||||
private TradeOrderQueryService tradeOrderQueryService;
|
||||
@Resource
|
||||
private HotelZaxOrderApi hotelZaxOrderApi;
|
||||
@Resource
|
||||
private DeliveryExpressService deliveryExpressService;
|
||||
@Resource
|
||||
private AfterSaleService afterSaleService;
|
||||
@@ -111,8 +116,25 @@ public class AppTradeOrderController {
|
||||
@Operation(summary = "更新订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> updateOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) {
|
||||
tradeOrderUpdateService.updateOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
|
||||
notifyReqDTO.getPayOrderId());
|
||||
Long payOrderId = notifyReqDTO.getPayOrderId();
|
||||
// 酒店与商城共用 mall 支付应用:商户单号可能与 trade_order.id 撞号,必须先按支付单认酒店单
|
||||
if (hotelZaxOrderApi.handlePayNotify(notifyReqDTO.getMerchantOrderId(), payOrderId)) {
|
||||
return success(true);
|
||||
}
|
||||
Long merchantOrderId;
|
||||
try {
|
||||
merchantOrderId = Long.valueOf(notifyReqDTO.getMerchantOrderId());
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException(ORDER_NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
tradeOrderUpdateService.updateOrderPaid(merchantOrderId, payOrderId);
|
||||
} catch (ServiceException ex) {
|
||||
if (hotelZaxOrderApi.handlePayNotify(notifyReqDTO.getMerchantOrderId(), payOrderId)) {
|
||||
return success(true);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
return success(true);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -32,6 +32,9 @@ public class AppTradeOrderPageItemRespVO {
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "支付超时时间")
|
||||
private LocalDateTime payExpireTime;
|
||||
|
||||
// ========== 价格 + 支付基本信息 ==========
|
||||
|
||||
@Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
|
||||
+5
-1
@@ -205,7 +205,11 @@ public interface TradeOrderConvert {
|
||||
// 转化 List
|
||||
List<AppTradeOrderPageItemRespVO> orderVOs = CollectionUtils.convertList(pageResult.getList(), order -> {
|
||||
List<TradeOrderItemDO> xOrderItems = orderItemMap.get(order.getId());
|
||||
return convert02(order, xOrderItems);
|
||||
AppTradeOrderPageItemRespVO vo = convert02(order, xOrderItems);
|
||||
if (vo != null && order.getCreateTime() != null) {
|
||||
vo.setPayExpireTime(order.getCreateTime().plusMinutes(10));
|
||||
}
|
||||
return vo;
|
||||
});
|
||||
return new PageResult<>(orderVOs, pageResult.getTotal());
|
||||
}
|
||||
|
||||
@@ -225,4 +225,14 @@ iot:
|
||||
|
||||
# 插件配置
|
||||
pf4j:
|
||||
pluginsDir: ${user.home}/plugins # 插件目录
|
||||
pluginsDir: ${user.home}/plugins # 插件目录
|
||||
|
||||
# 酒店供应(资安心开放平台中转,密钥勿下发小程序)
|
||||
xrhome:
|
||||
hotel:
|
||||
zax:
|
||||
enabled: false
|
||||
# 必须含到 /third/party/open,例:http://localhost:9999/api/supermarket/third/party/open
|
||||
base-url: ""
|
||||
app-key: ""
|
||||
app-secret: ""
|
||||
@@ -261,4 +261,22 @@ justauth:
|
||||
--- #################### iot相关配置 TODO 芋艿【IOT】:再瞅瞅 ####################
|
||||
pf4j:
|
||||
# pluginsDir: /tmp/
|
||||
pluginsDir: ../plugins
|
||||
pluginsDir: ../plugins
|
||||
|
||||
--- #################### 酒店供应(资安心开放平台中转,密钥勿下发小程序) ####################
|
||||
xrhome:
|
||||
hotel:
|
||||
zax:
|
||||
enabled: true
|
||||
# 本地网关:不要带 /api(全局 Strip 第一段后路由是 /supermarket/**)
|
||||
# 生产示例:https://www.zianxin.com/api/supermarket/third/party/open
|
||||
base-url: "http://localhost:9999/supermarket/third/party/open"
|
||||
app-key: "OA2112419111353806850"
|
||||
app-secret: "13bdf82c3c3046119942e65c6d51a665f7010d1c"
|
||||
# 回调无租户头时落库租户;资安心开放应用回调根地址示例:
|
||||
# callbackBaseUrl=http://localhost:48089/app-api
|
||||
# callbackResourcePath=/hotel/resource/notify
|
||||
# callbackOrderPath=/order/status/notify
|
||||
callback-tenant-id: 1
|
||||
# 微信支付应用(支付管理→应用信息),默认复用商城 mall
|
||||
pay-app-key: mall
|
||||
@@ -243,3 +243,19 @@ justauth:
|
||||
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
|
||||
timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟
|
||||
|
||||
xrhome:
|
||||
hotel:
|
||||
zax:
|
||||
enabled: true
|
||||
# 本地网关:不要带 /api(全局 Strip 第一段后路由是 /supermarket/**)
|
||||
# 生产示例:https://www.zianxin.com/api/supermarket/third/party/open
|
||||
base-url: "https://www.zianxin.com/api/supermarket/third/party/open"
|
||||
app-key: "OA2112419111353806850"
|
||||
app-secret: "13bdf82c3c3046119942e65c6d51a665f7010d1c"
|
||||
# 回调无租户头时落库租户;资安心开放应用回调根地址示例:
|
||||
# callbackBaseUrl=http://localhost:48089/app-api
|
||||
# callbackResourcePath=/hotel/resource/notify
|
||||
# callbackOrderPath=/order/status/notify
|
||||
callback-tenant-id: 1
|
||||
# 微信支付应用(支付管理→应用信息),默认复用商城 mall
|
||||
pay-app-key: mall
|
||||
@@ -245,6 +245,9 @@ yudao:
|
||||
security:
|
||||
permit-all_urls:
|
||||
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
|
||||
- /app-api/hotel/resource/notify # 资安心 → 小如 资源回调
|
||||
- /app-api/order/status/notify # 资安心 → 小如 订单状态回调
|
||||
- /app-api/hotel/order/status/notify
|
||||
websocket:
|
||||
enable: true # websocket的开关
|
||||
path: /infra/ws # 路径
|
||||
@@ -284,6 +287,9 @@ yudao:
|
||||
- /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
|
||||
- /jmreport/* # 积木报表,无法携带租户编号
|
||||
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
|
||||
- /app-api/hotel/resource/notify # 资安心酒店资源回调
|
||||
- /app-api/order/status/notify # 资安心酒店订单状态回调
|
||||
- /app-api/hotel/order/status/notify # 资安心酒店订单状态回调(备用路径)
|
||||
ignore-tables:
|
||||
- system_tenant
|
||||
- system_tenant_package
|
||||
@@ -346,7 +352,7 @@ yudao:
|
||||
end-code: 9999 # 这里配置 9999 的原因是,测试方便。
|
||||
trade:
|
||||
order:
|
||||
pay-expire-time: 2h # 支付的过期时间
|
||||
pay-expire-time: 10m # 待支付超时自动取消
|
||||
receive-expire-time: 14d # 收货的过期时间
|
||||
comment-expire-time: 7d # 评论的过期时间
|
||||
express:
|
||||
|
||||
Reference in new issue
Block a user