酒店增加本地维护
This commit is contained in:
1 parent
80b4d1e9cc
commit
e2ae48d0e5
17 files changed
+630
-60
No files matched your search
+7
-1
@@ -50,9 +50,15 @@ public class SmHotelEntity extends Model<SmHotelEntity> {
|
||||
@Schema(description = "数据来源(字典 sm_hotel_data_source)")
|
||||
private String sheetName;
|
||||
|
||||
@Schema(description = "ACTIVE启用 / OFFLINE上游下线停用")
|
||||
@Schema(description = "上架状态:ACTIVE上架 / OFFLINE下架(本地可改;第三方由上游差集写入)")
|
||||
private String upstreamStatus;
|
||||
|
||||
@Schema(description = "本地自动滚加:0关 1开(仅数据来源=本地)")
|
||||
private Integer localAutoRoll;
|
||||
|
||||
@Schema(description = "本地滚加维持天数(今天起共 N 天,1~366)")
|
||||
private Integer localRollHorizonDays;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
+9
@@ -156,6 +156,15 @@ public class ShangyouHotelProperties {
|
||||
/** 滚价每家间隔毫秒,减轻上游限流/繁忙(默认 30) */
|
||||
private int priceRollIntervalMs = 30;
|
||||
|
||||
/**
|
||||
* 本地酒店自动滚加日历 Cron(仅 spring-schedule-enabled=true 时生效)。
|
||||
* 默认每天 03:30:0 30 3 * * ?
|
||||
*/
|
||||
private String localDailyRollCron = "0 30 3 * * ?";
|
||||
|
||||
/** 是否启用本地酒店自动滚加(按酒店 local_auto_roll 配置补齐缺失日历) */
|
||||
private boolean localDailyRollEnabled = true;
|
||||
|
||||
/** 文档:全量房价房量每天最多次数 */
|
||||
private int fullPriceSyncMaxPerDay = 3;
|
||||
|
||||
|
||||
+3
-3
@@ -40,7 +40,7 @@ public class SmHotelController {
|
||||
.eq(StrUtil.isNotBlank(query.getSheetName()), SmHotelEntity::getSheetName, query.getSheetName())
|
||||
.orderByDesc(SmHotelEntity::getCreateTime)
|
||||
.orderByDesc(SmHotelEntity::getId);
|
||||
// 启用:含未写状态的老数据;停用:仅 OFFLINE;空=全部
|
||||
// 上架(ACTIVE):含未写状态的老数据;下架:仅 OFFLINE;空=全部
|
||||
if ("OFFLINE".equalsIgnoreCase(query.getUpstreamStatus())) {
|
||||
wrapper.eq(SmHotelEntity::getUpstreamStatus, "OFFLINE");
|
||||
}
|
||||
@@ -55,14 +55,14 @@ public class SmHotelController {
|
||||
/** 上传文件会立刻 transferTo 临时目录,勿加 @SysLog */
|
||||
@Operation(summary = "提交Excel异步导入(多Sheet流式)")
|
||||
@PostMapping("/import")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_import')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<SmHotelImportResultVO> importExcel(@RequestParam("file") MultipartFile file) {
|
||||
return hotelService.startImport(file);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询酒店导入进度")
|
||||
@GetMapping("/import/status/{taskId}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_import')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<SmHotelImportResultVO> importStatus(@PathVariable String taskId) {
|
||||
return hotelService.getImportStatus(taskId);
|
||||
}
|
||||
|
||||
+47
-21
@@ -1,6 +1,7 @@
|
||||
package com.bsd.pjcloud.supermarket.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelPriceConfigEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
@@ -46,15 +47,15 @@ public class SmHotelMidController {
|
||||
}
|
||||
|
||||
@SysLog("保存酒店售价调整配置")
|
||||
@Operation(summary = "保存全局售价调整配置(正数加价、负数折扣;仅保存,不重算库存卖价)")
|
||||
@Operation(summary = "保存全局售价调整配置(仅第三方来源;正数加价、负数折扣;仅保存,不重算库存卖价)")
|
||||
@PutMapping("/priceConfig")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_edit')")
|
||||
public R<Boolean> savePriceConfig(@RequestBody SmHotelPriceConfigEntity entity) {
|
||||
return priceConfigService.saveOrUpdateConfig(entity);
|
||||
}
|
||||
|
||||
@SysLog("全局同步酒店售价(按配置重算产品/日历,不动订单)")
|
||||
@Operation(summary = "按当前配置重算全部产品与日历卖价(不动订单)")
|
||||
@SysLog("全局同步酒店售价(仅第三方来源,按配置重算产品/日历,不动订单与本地酒店)")
|
||||
@Operation(summary = "按当前配置重算第三方酒店产品与日历卖价(排除数据来源=本地;不动订单)")
|
||||
@PostMapping("/priceConfig/resyncSell")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_edit')")
|
||||
public R<Map<String, Object>> resyncSellPrices() {
|
||||
@@ -79,56 +80,56 @@ public class SmHotelMidController {
|
||||
|
||||
@Operation(summary = "从上游拉取房型产品并加价落库")
|
||||
@PostMapping("/mid/pull/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pull(@PathVariable Long id) {
|
||||
return midService.pullFromUpstream(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "仅拉取上游房型(getHotelComplexList,不拉房价)")
|
||||
@PostMapping("/mid/pullRooms/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pullRooms(@PathVariable Long id) {
|
||||
return midService.pullRoomsFromUpstream(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "仅拉取上游房价样例(getRoomPrice 今天 1 天)")
|
||||
@PostMapping("/mid/pullPrice/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pullPrice(@PathVariable Long id) {
|
||||
return midService.pullPriceFromUpstream(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "仅拉取上游产品样例(getRoomPrice 今天 1 天落产品)")
|
||||
@PostMapping("/mid/pullProducts/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pullProducts(@PathVariable Long id) {
|
||||
return midService.pullProductsFromUpstream(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "推送到下游销售方")
|
||||
@PostMapping("/mid/push/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_push')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> push(@PathVariable Long id) {
|
||||
return midService.pushToDownstream(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "拉取上游并推送下游")
|
||||
@PostMapping("/mid/pullAndPush/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_push')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pullAndPush(@PathVariable Long id) {
|
||||
return midService.pullAndPush(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "批量拉取并推送")
|
||||
@PostMapping("/mid/pullAndPush/batch")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_push')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pullAndPushBatch(@RequestBody List<Long> ids) {
|
||||
return midService.pullAndPushBatch(ids);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询上游账号已授权酒店列表")
|
||||
@GetMapping("/mid/authorizedHotels")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_view')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> authorizedHotels(@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false, defaultValue = "1") long current,
|
||||
@RequestParam(required = false, defaultValue = "20") long size,
|
||||
@@ -149,7 +150,7 @@ public class SmHotelMidController {
|
||||
@SysLog("同步上游酒店列表并差集")
|
||||
@Operation(summary = "异步同步 getHotelList + Redis 差集(立刻返回 logId;须选数据来源)")
|
||||
@PostMapping("/mid/syncHotelList")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> syncHotelList(
|
||||
@RequestParam(required = false, defaultValue = "true") boolean autoImport,
|
||||
@RequestParam("dataSource") String dataSource) {
|
||||
@@ -158,7 +159,7 @@ public class SmHotelMidController {
|
||||
|
||||
@Operation(summary = "同步日志分页")
|
||||
@GetMapping("/mid/syncLog/page")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_view')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Page<SmHotelSyncLogEntity>> syncLogPage(@ParameterObject Page<SmHotelSyncLogEntity> page,
|
||||
@ParameterObject SmHotelSyncLogEntity query) {
|
||||
return R.ok(syncService.pageSyncLogs(page, query));
|
||||
@@ -166,7 +167,7 @@ public class SmHotelMidController {
|
||||
|
||||
@Operation(summary = "查询单次同步进度/结果")
|
||||
@GetMapping("/mid/syncLog/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_view')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<SmHotelSyncLogEntity> syncLog(@PathVariable Long id) {
|
||||
return syncService.getSyncLog(id);
|
||||
}
|
||||
@@ -174,7 +175,7 @@ public class SmHotelMidController {
|
||||
@SysLog("终止酒店同步死任务")
|
||||
@Operation(summary = "终止仍显示进行中的同步日志(死任务)")
|
||||
@PostMapping("/mid/syncLog/{id}/abort")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Boolean> abortSyncLog(@PathVariable Long id) {
|
||||
return syncService.abortSyncLog(id);
|
||||
}
|
||||
@@ -182,21 +183,21 @@ public class SmHotelMidController {
|
||||
@SysLog("清理酒店同步卡死任务")
|
||||
@Operation(summary = "清理长时间无进度的 RUNNING 同步日志")
|
||||
@PostMapping("/mid/syncLog/abortStale")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Integer> abortStaleSyncLogs() {
|
||||
return R.ok(syncService.abortStaleRunningSyncLogs());
|
||||
}
|
||||
|
||||
@Operation(summary = "日更时间配置(只读展示)")
|
||||
@GetMapping("/mid/syncSchedule")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_view')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> syncSchedule() {
|
||||
return R.ok(syncService.getSyncSchedule());
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查看酒店差集 new|offline")
|
||||
@GetMapping("/mid/hotelDiff")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_view')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> hotelDiff(@RequestParam(required = false, defaultValue = "new") String type,
|
||||
@RequestParam(required = false, defaultValue = "1") long current,
|
||||
@RequestParam(required = false, defaultValue = "20") long size) {
|
||||
@@ -248,6 +249,15 @@ public class SmHotelMidController {
|
||||
return midService.saveLocalDaily(hotelId, daily);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店批量生成/调价日历")
|
||||
@Operation(summary = "本地酒店按日期区间批量生成或调价(FILL 补齐缺失 / ADJUST 改已有)")
|
||||
@PostMapping("/local/{hotelId}/daily/batch")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Map<String, Object>> batchLocalDaily(@PathVariable Long hotelId,
|
||||
@RequestBody SmHotelLocalDailyBatchDTO req) {
|
||||
return midService.batchLocalDaily(hotelId, req);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店删除日历")
|
||||
@DeleteMapping("/local/{hotelId}/daily/{dailyId}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@@ -258,7 +268,7 @@ public class SmHotelMidController {
|
||||
@SysLog("手动执行滚一天房价")
|
||||
@Operation(summary = "异步滚一天房价(立刻返回 logId)")
|
||||
@PostMapping("/mid/rollDailyPrice")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> rollDailyPrice() {
|
||||
R<SmHotelSyncLogEntity> started = syncService.startPriceRollAsync("MANUAL");
|
||||
if (started.getCode() != 0) {
|
||||
@@ -271,10 +281,26 @@ public class SmHotelMidController {
|
||||
return R.ok(data, started.getMsg());
|
||||
}
|
||||
|
||||
@SysLog("手动执行本地酒店滚加日历")
|
||||
@Operation(summary = "异步本地滚加:对开启配置的本地酒店补齐今天起 N 天缺失日历")
|
||||
@PostMapping("/mid/rollLocalDaily")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Map<String, Object>> rollLocalDaily() {
|
||||
R<SmHotelSyncLogEntity> started = syncService.startLocalDailyRollAsync("MANUAL");
|
||||
if (started.getCode() != 0) {
|
||||
return R.failed(started.getMsg());
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("async", true);
|
||||
data.put("logId", started.getData().getId());
|
||||
data.put("status", started.getData().getStatus());
|
||||
return R.ok(data, started.getMsg());
|
||||
}
|
||||
|
||||
@SysLog("全量拉取启用酒店详情")
|
||||
@Operation(summary = "异步拉取全部启用酒店详情(房型+产品+未来房价),立刻返回 logId")
|
||||
@PostMapping("/mid/pullAllActiveDetails")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pullAllActiveDetails(
|
||||
@RequestParam(required = false) Integer limit) {
|
||||
R<SmHotelSyncLogEntity> started = syncService.startDetailPullAsync(limit, "MANUAL");
|
||||
@@ -292,7 +318,7 @@ public class SmHotelMidController {
|
||||
@SysLog("重试同步失败酒店")
|
||||
@Operation(summary = "根据某次同步日志的失败明细,只重试失败酒店(详情/滚价)")
|
||||
@PostMapping("/mid/retryFailedFromLog")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> retryFailedFromLog(@RequestParam Long logId) {
|
||||
R<SmHotelSyncLogEntity> started = syncService.startRetryFailedAsync(logId);
|
||||
if (started.getCode() != 0) {
|
||||
|
||||
+2
-2
@@ -29,14 +29,14 @@ public class SmHotelUpstreamController {
|
||||
|
||||
@Operation(summary = "同步上游酒店信息到本地")
|
||||
@PostMapping("/sync/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<JSONObject> sync(@PathVariable Long id) {
|
||||
return upstreamService.syncHotelInfo(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "批量同步上游酒店信息")
|
||||
@PostMapping("/sync/batch")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> syncBatch(@RequestBody List<Long> ids) {
|
||||
return upstreamService.syncHotelInfoBatch(ids);
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.bsd.pjcloud.supermarket.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 本地酒店日历批量生成 / 调价。
|
||||
* <ul>
|
||||
* <li>FILL:按产品默认价/库存补齐缺失日期(已有行跳过)</li>
|
||||
* <li>ADJUST:对区间内已有行改价/配额/房态(保留已售);可选同时补齐缺失</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "本地酒店日历批量请求")
|
||||
public class SmHotelLocalDailyBatchDTO {
|
||||
|
||||
@Schema(description = "开始日期(含)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDate startDate;
|
||||
|
||||
@Schema(description = "结束日期(含)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDate endDate;
|
||||
|
||||
@Schema(description = "产品编码列表,空=该酒店全部产品")
|
||||
private List<String> productCodes;
|
||||
|
||||
@Schema(description = "FILL=补齐缺失;ADJUST=调整已有", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String mode;
|
||||
|
||||
@Schema(description = "ADJUST 时是否同时按产品默认补齐缺失日期,默认 false")
|
||||
private Boolean fillMissing;
|
||||
|
||||
@Schema(description = "覆盖成本价;FILL 时空则用产品成本价;ADJUST 时空表示不改")
|
||||
private BigDecimal costPrice;
|
||||
|
||||
@Schema(description = "覆盖卖价;FILL 时空则用产品卖价;ADJUST 时空表示不改")
|
||||
private BigDecimal sellPrice;
|
||||
|
||||
@Schema(description = "覆盖当日配额;FILL 时空则用产品库存;ADJUST 时空表示不改")
|
||||
private Integer stock;
|
||||
|
||||
@Schema(description = "覆盖房态 0关1开;FILL 时空默认开房;ADJUST 时空表示不改")
|
||||
private Integer dailyStatus;
|
||||
}
|
||||
+18
-1
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Component;
|
||||
* YAML 可配 Cron 的日更入口(与 XXL Job 二选一即可)。
|
||||
* <p>
|
||||
* 开关:{@code shangyou-hotel.spring-schedule-enabled}<br>
|
||||
* 时间:{@code hotel-list-daily-cron} / {@code price-roll-daily-cron}
|
||||
* 时间:{@code hotel-list-daily-cron} / {@code price-roll-daily-cron} / {@code local-daily-roll-cron}
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -55,4 +55,21 @@ public class SmHotelShangyouSpringSchedule {
|
||||
log.error("|酒店日更|Spring 滚价异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${shangyou-hotel.local-daily-roll-cron:0 30 3 * * ?}")
|
||||
public void scheduledLocalDailyRoll() {
|
||||
if (!properties.isSpringScheduleEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (!properties.isLocalDailyRollEnabled()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
log.info("|酒店日更|Spring Cron 触发本地滚加|cron={}|", properties.getLocalDailyRollCron());
|
||||
syncService.jobRollLocalDaily("CRON");
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("|酒店日更|Spring 本地滚加异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-1
@@ -13,7 +13,8 @@ import java.util.Map;
|
||||
* 上游酒店日更:
|
||||
* <ul>
|
||||
* <li>{@link #HANDLER_LIST_SYNC}:getHotelList + Redis 差集 + 可选自动导入新增</li>
|
||||
* <li>{@link #HANDLER_PRICE_ROLL}:对本地酒店只拉 today+offset 一天房价</li>
|
||||
* <li>{@link #HANDLER_PRICE_ROLL}:对第三方酒店只拉 today+offset 一天房价</li>
|
||||
* <li>{@link #HANDLER_LOCAL_ROLL}:本地酒店按配置补齐日历窗口</li>
|
||||
* </ul>
|
||||
* 频率在 XXL-JOB 控制台配 Cron;开关与参数在 shangyou-hotel.*。
|
||||
*/
|
||||
@@ -26,6 +27,8 @@ public class SmHotelShangyouSyncJob {
|
||||
|
||||
public static final String HANDLER_PRICE_ROLL = "smHotelPriceRollJobHandler";
|
||||
|
||||
public static final String HANDLER_LOCAL_ROLL = "smHotelLocalDailyRollJobHandler";
|
||||
|
||||
private final SmHotelSyncService syncService;
|
||||
|
||||
@XxlJob(HANDLER_LIST_SYNC)
|
||||
@@ -53,4 +56,17 @@ public class SmHotelShangyouSyncJob {
|
||||
XxlJobHelper.handleFail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@XxlJob(HANDLER_LOCAL_ROLL)
|
||||
public void smHotelLocalDailyRollJobHandler() {
|
||||
try {
|
||||
Map<String, Object> r = syncService.jobRollLocalDaily();
|
||||
XxlJobHelper.log("hotel local daily roll: " + r);
|
||||
XxlJobHelper.handleSuccess(String.valueOf(r));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("smHotelLocalDailyRollJobHandler 异常", e);
|
||||
XxlJobHelper.handleFail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-6
@@ -26,14 +26,16 @@ public interface SmHotelDailyMapper extends PjcloudBaseMapper<SmHotelDailyEntity
|
||||
int insertBatch(@Param("list") List<SmHotelDailyEntity> list);
|
||||
|
||||
/**
|
||||
* 按底价重算日历卖价:sell = ROUND(cost * rate, 2)。不改订单。
|
||||
* 按底价重算日历卖价:sell = ROUND(cost * rate, 2)。仅第三方来源酒店;本地 sheet_name=local 不参与。
|
||||
*/
|
||||
@Update("""
|
||||
UPDATE canteen_hotel_daily
|
||||
SET sell_price = ROUND(cost_price * #{rate}, 2),
|
||||
update_time = NOW()
|
||||
WHERE del_flag = '0'
|
||||
AND cost_price IS NOT NULL
|
||||
UPDATE canteen_hotel_daily d
|
||||
INNER JOIN canteen_hotel h ON h.id = d.hotel_id AND h.del_flag = '0'
|
||||
SET d.sell_price = ROUND(d.cost_price * #{rate}, 2),
|
||||
d.update_time = NOW()
|
||||
WHERE d.del_flag = '0'
|
||||
AND d.cost_price IS NOT NULL
|
||||
AND (h.sheet_name IS NULL OR LOWER(TRIM(h.sheet_name)) <> 'local')
|
||||
""")
|
||||
int resyncSellPriceByCost(@Param("rate") BigDecimal rate);
|
||||
|
||||
|
||||
+8
-6
@@ -18,14 +18,16 @@ public interface SmHotelProductMapper extends PjcloudBaseMapper<SmHotelProductEn
|
||||
int insertBatch(@Param("list") List<SmHotelProductEntity> list);
|
||||
|
||||
/**
|
||||
* 按底价重算卖价:sell = ROUND(cost * rate, 2)。不改订单。
|
||||
* 按底价重算卖价:sell = ROUND(cost * rate, 2)。仅第三方来源酒店;本地 sheet_name=local 不参与。
|
||||
*/
|
||||
@Update("""
|
||||
UPDATE canteen_hotel_product
|
||||
SET sell_price = ROUND(cost_price * #{rate}, 2),
|
||||
update_time = NOW()
|
||||
WHERE del_flag = '0'
|
||||
AND cost_price IS NOT NULL
|
||||
UPDATE canteen_hotel_product p
|
||||
INNER JOIN canteen_hotel h ON h.id = p.hotel_id AND h.del_flag = '0'
|
||||
SET p.sell_price = ROUND(p.cost_price * #{rate}, 2),
|
||||
p.update_time = NOW()
|
||||
WHERE p.del_flag = '0'
|
||||
AND p.cost_price IS NOT NULL
|
||||
AND (h.sheet_name IS NULL OR LOWER(TRIM(h.sheet_name)) <> 'local')
|
||||
""")
|
||||
int resyncSellPriceByCost(@Param("rate") BigDecimal rate);
|
||||
|
||||
|
||||
+7
@@ -1,6 +1,7 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity;
|
||||
@@ -81,4 +82,10 @@ public interface SmHotelMidService {
|
||||
|
||||
/** 本地数据来源:删除日历行 */
|
||||
R<Boolean> removeLocalDaily(Long hotelId, Long dailyId);
|
||||
|
||||
/**
|
||||
* 本地数据来源:按日期区间批量生成或调价。
|
||||
* FILL 补齐缺失(产品默认价);ADJUST 改已有行(可选再补齐)。
|
||||
*/
|
||||
R<Map<String, Object>> batchLocalDaily(Long hotelId, SmHotelLocalDailyBatchDTO req);
|
||||
}
|
||||
+8
@@ -72,4 +72,12 @@ public interface SmHotelSyncService {
|
||||
Map<String, Object> jobRollDailyPrice();
|
||||
|
||||
Map<String, Object> jobRollDailyPrice(String triggerType);
|
||||
|
||||
/** 提交本地酒店自动滚加(补齐今天起 N 天缺失日历) */
|
||||
R<SmHotelSyncLogEntity> startLocalDailyRollAsync(String triggerType);
|
||||
|
||||
/** XXL / 定时:本地滚加 */
|
||||
Map<String, Object> jobRollLocalDaily();
|
||||
|
||||
Map<String, Object> jobRollLocalDaily(String triggerType);
|
||||
}
|
||||
+208
-3
@@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties;
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO;
|
||||
import com.bsd.pjcloud.supermarket.entity.*;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelClient;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
@@ -47,6 +48,9 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
/** 上游单次房价查询建议不超过 31 天 */
|
||||
private static final int ROOM_PRICE_CHUNK_DAYS = 30;
|
||||
|
||||
/** 本地日历单次批量最长天数(含首尾) */
|
||||
private static final int LOCAL_DAILY_BATCH_MAX_DAYS = 366;
|
||||
|
||||
private final ShangyouHotelClient upstreamClient;
|
||||
|
||||
private final ShangyouHotelProperties upstreamProperties;
|
||||
@@ -668,6 +672,18 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
if (room == null || StrUtil.isBlank(room.getRoomTypeCode())) {
|
||||
return R.failed("房型编码不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(room.getRoomTypeName())) {
|
||||
return R.failed("房型名称不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(room.getBedType())) {
|
||||
return R.failed("床型不能为空");
|
||||
}
|
||||
if (room.getCapacity() == null) {
|
||||
return R.failed("可住人数不能为空");
|
||||
}
|
||||
if (room.getRoomNumber() == null) {
|
||||
return R.failed("房间数不能为空");
|
||||
}
|
||||
room.setHotelId(hotel.getId());
|
||||
room.setHotelCode(hotel.getHotelCode());
|
||||
room.setGroupCode(hotel.getGroupCode());
|
||||
@@ -716,9 +732,24 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
if (product == null || StrUtil.isBlank(product.getProductCode())) {
|
||||
return R.failed("产品编码不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(product.getProductName())) {
|
||||
return R.failed("产品名称不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(product.getRoomTypeCode())) {
|
||||
return R.failed("所属房型编码不能为空");
|
||||
}
|
||||
if (product.getMealCount() == null) {
|
||||
return R.failed("早餐份数不能为空");
|
||||
}
|
||||
if (product.getCostPrice() == null) {
|
||||
return R.failed("成本价不能为空");
|
||||
}
|
||||
if (product.getSellPrice() == null) {
|
||||
return R.failed("卖价不能为空");
|
||||
}
|
||||
if (product.getStock() == null) {
|
||||
return R.failed("库存不能为空");
|
||||
}
|
||||
Long roomCnt = roomMapper.selectCount(Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
.eq(SmHotelRoomEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelRoomEntity::getRoomTypeCode, product.getRoomTypeCode().trim()));
|
||||
@@ -780,6 +811,18 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
if (StrUtil.isBlank(daily.getProductCode())) {
|
||||
return R.failed("产品编码不能为空");
|
||||
}
|
||||
if (daily.getCostPrice() == null) {
|
||||
return R.failed("成本价不能为空");
|
||||
}
|
||||
if (daily.getSellPrice() == null) {
|
||||
return R.failed("卖价不能为空");
|
||||
}
|
||||
if (daily.getStock() == null) {
|
||||
return R.failed("当日配额不能为空");
|
||||
}
|
||||
if (daily.getDailyStatus() == null) {
|
||||
return R.failed("房态不能为空");
|
||||
}
|
||||
SmHotelProductEntity product = productMapper.selectOne(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelProductEntity::getProductCode, daily.getProductCode().trim())
|
||||
@@ -845,8 +888,165 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
return R.ok(dailyMapper.deleteById(dailyId) > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Map<String, Object>> batchLocalDaily(Long hotelId, SmHotelLocalDailyBatchDTO req) {
|
||||
SmHotelEntity hotel = requireLocalHotel(hotelId);
|
||||
if (req == null || req.getStartDate() == null || req.getEndDate() == null) {
|
||||
return R.failed("请选择开始/结束日期");
|
||||
}
|
||||
if (req.getEndDate().isBefore(req.getStartDate())) {
|
||||
return R.failed("结束日期不能早于开始日期");
|
||||
}
|
||||
long dayCount = req.getEndDate().toEpochDay() - req.getStartDate().toEpochDay() + 1;
|
||||
if (dayCount > LOCAL_DAILY_BATCH_MAX_DAYS) {
|
||||
return R.failed("单次最多生成/调整 " + 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) {
|
||||
return R.failed("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) {
|
||||
return R.failed("调整模式下请至少填写一项:成本价/卖价/配额/房态,或勾选同时补齐缺失");
|
||||
}
|
||||
|
||||
List<SmHotelProductEntity> products = loadLocalProductsForBatch(hotelId, req.getProductCodes());
|
||||
if (products.isEmpty()) {
|
||||
return R.failed("没有可操作的产品,请先维护产品");
|
||||
}
|
||||
for (SmHotelProductEntity p : products) {
|
||||
if (p.getCostPrice() == null && req.getCostPrice() == null && (fillMode || fillMissing)) {
|
||||
return R.failed("产品 " + p.getProductCode() + " 未设置成本价,请先维护产品或在本次填写覆盖成本价");
|
||||
}
|
||||
if (p.getSellPrice() == null && req.getSellPrice() == null && (fillMode || fillMissing)) {
|
||||
return R.failed("产品 " + p.getProductCode() + " 未设置卖价,请先维护产品或在本次填写覆盖卖价");
|
||||
}
|
||||
if (p.getStock() == null && req.getStock() == null && (fillMode || fillMissing)) {
|
||||
return R.failed("产品 " + p.getProductCode() + " 未设置库存,请先维护产品或在本次填写覆盖配额");
|
||||
}
|
||||
}
|
||||
|
||||
List<String> productCodes = products.stream().map(SmHotelProductEntity::getProductCode).toList();
|
||||
List<SmHotelDailyEntity> existRows = dailyMapper.selectList(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotelId)
|
||||
.in(SmHotelDailyEntity::getProductCode, productCodes)
|
||||
.ge(SmHotelDailyEntity::getStayDate, req.getStartDate())
|
||||
.le(SmHotelDailyEntity::getStayDate, req.getEndDate()));
|
||||
Map<String, SmHotelDailyEntity> existMap = new HashMap<>(Math.max(16, existRows.size() * 2));
|
||||
for (SmHotelDailyEntity d : existRows) {
|
||||
existMap.put(d.getProductCode() + "|" + d.getStayDate(), d);
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Long tenantId = resolveTenantId();
|
||||
List<SmHotelDailyEntity> toInsert = new ArrayList<>();
|
||||
List<SmHotelDailyEntity> toUpdate = new ArrayList<>();
|
||||
int skippedExist = 0;
|
||||
int skippedStock = 0;
|
||||
|
||||
for (SmHotelProductEntity product : products) {
|
||||
for (LocalDate day = req.getStartDate(); !day.isAfter(req.getEndDate()); day = day.plusDays(1)) {
|
||||
String key = product.getProductCode() + "|" + day;
|
||||
SmHotelDailyEntity exist = existMap.get(key);
|
||||
if (exist == null) {
|
||||
if (fillMode || (adjustMode && fillMissing)) {
|
||||
toInsert.add(buildLocalDailyFromProduct(hotel, product, day, req, tenantId, now));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (fillMode) {
|
||||
skippedExist++;
|
||||
continue;
|
||||
}
|
||||
// ADJUST:改已有行,未传的字段保持原值
|
||||
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);
|
||||
exist.setUpdateTime(now);
|
||||
toUpdate.add(exist);
|
||||
}
|
||||
}
|
||||
|
||||
insertDailiesBatch(toInsert);
|
||||
if (CollUtil.isNotEmpty(toUpdate)) {
|
||||
Db.updateBatchById(toUpdate, PERSIST_BATCH);
|
||||
}
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("mode", mode);
|
||||
data.put("startDate", req.getStartDate().toString());
|
||||
data.put("endDate", req.getEndDate().toString());
|
||||
data.put("productCount", products.size());
|
||||
data.put("dayCount", dayCount);
|
||||
data.put("inserted", toInsert.size());
|
||||
data.put("updated", toUpdate.size());
|
||||
data.put("skippedExist", skippedExist);
|
||||
data.put("skippedStockLtSold", skippedStock);
|
||||
String msg = fillMode
|
||||
? String.format("已补齐 %d 天行,跳过已有 %d", toInsert.size(), skippedExist)
|
||||
: String.format("已调整 %d 行,新补齐 %d,配额不足跳过 %d", toUpdate.size(), toInsert.size(), skippedStock);
|
||||
return R.ok(data, msg);
|
||||
}
|
||||
|
||||
/** 按产品编码筛选;空列表=全部产品 */
|
||||
private List<SmHotelProductEntity> loadLocalProductsForBatch(Long hotelId, List<String> productCodes) {
|
||||
var wrapper = Wrappers.<SmHotelProductEntity>lambdaQuery().eq(SmHotelProductEntity::getHotelId, hotelId);
|
||||
if (CollUtil.isNotEmpty(productCodes)) {
|
||||
List<String> codes = productCodes.stream()
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (codes.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
wrapper.in(SmHotelProductEntity::getProductCode, codes);
|
||||
}
|
||||
return productMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
private SmHotelDailyEntity buildLocalDailyFromProduct(SmHotelEntity hotel, SmHotelProductEntity product,
|
||||
LocalDate stayDate, SmHotelLocalDailyBatchDTO req, Long tenantId, LocalDateTime now) {
|
||||
SmHotelDailyEntity daily = new SmHotelDailyEntity();
|
||||
daily.setHotelId(hotel.getId());
|
||||
daily.setProductId(product.getId());
|
||||
daily.setProductCode(product.getProductCode());
|
||||
daily.setRoomTypeCode(product.getRoomTypeCode());
|
||||
daily.setStayDate(stayDate);
|
||||
daily.setCostPrice(req.getCostPrice() != null ? req.getCostPrice() : product.getCostPrice());
|
||||
daily.setSellPrice(req.getSellPrice() != null ? req.getSellPrice() : product.getSellPrice());
|
||||
daily.setStock(req.getStock() != null ? req.getStock() : product.getStock());
|
||||
daily.setSold(0);
|
||||
daily.setDailyStatus(req.getDailyStatus() != null ? req.getDailyStatus() : 1);
|
||||
fillNewMeta(daily, tenantId, now);
|
||||
return daily;
|
||||
}
|
||||
|
||||
private SmHotelEntity requireLocalHotel(Long hotelId) {
|
||||
SmHotelEntity hotel = requireHotel(hotelId);
|
||||
SmHotelEntity hotel = requireHotel(hotelId, false);
|
||||
if (!SmHotelDataSource.isLocal(hotel.getSheetName())) {
|
||||
throw new IllegalArgumentException("仅「本地」数据来源的酒店可手工维护房型/产品/日历");
|
||||
}
|
||||
@@ -854,6 +1054,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
}
|
||||
|
||||
private SmHotelEntity requireHotel(Long hotelId) {
|
||||
return requireHotel(hotelId, true);
|
||||
}
|
||||
|
||||
private SmHotelEntity requireHotel(Long hotelId, boolean requireListed) {
|
||||
if (hotelId == null) {
|
||||
throw new IllegalArgumentException("酒店ID不能为空");
|
||||
}
|
||||
@@ -864,8 +1068,9 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
if (StrUtil.isBlank(hotel.getHotelCode())) {
|
||||
throw new IllegalArgumentException("酒店缺少 hotelCode(对应上游 hotelId)");
|
||||
}
|
||||
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
|
||||
throw new IllegalArgumentException("酒店已因上游下线停用,禁止拉取/推送");
|
||||
if (requireListed && "OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
|
||||
throw new IllegalArgumentException(
|
||||
SmHotelDataSource.isLocal(hotel.getSheetName()) ? "酒店已下架,禁止拉取/推送" : "酒店已因上游下线停用,禁止拉取/推送");
|
||||
}
|
||||
return hotel;
|
||||
}
|
||||
|
||||
+9
-6
@@ -112,7 +112,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return R.failed("本地无此酒店,请先同步导入:" + hotelCode);
|
||||
}
|
||||
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
|
||||
return R.failed("酒店已停用,无法下单");
|
||||
return R.failed("酒店已下架,无法下单");
|
||||
}
|
||||
SmHotelProductEntity product = null;
|
||||
if (StrUtil.isNotBlank(productCode)) {
|
||||
@@ -146,7 +146,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
if (body.get("guestList") != null) {
|
||||
order.setGuestJson(JSONUtil.toJsonStr(body.get("guestList")));
|
||||
}
|
||||
order.setMarkupPercent(priceConfigService.currentMarkupPercent());
|
||||
// 全局加价仅针对第三方来源;本地酒店卖价按日历/产品原价,markup=0
|
||||
boolean localHotel = SmHotelDataSource.isLocal(hotel.getSheetName());
|
||||
order.setMarkupPercent(localHotel ? BigDecimal.ZERO : priceConfigService.currentMarkupPercent());
|
||||
order.setSellAmount(toDecimal(body.get("totalAmount")));
|
||||
if (product != null && product.getCostPrice() != null && order.getCount() != null) {
|
||||
order.setCostAmount(product.getCostPrice().multiply(BigDecimal.valueOf(order.getCount())));
|
||||
@@ -167,7 +169,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
baseMapper.insert(order);
|
||||
|
||||
// 本地酒店:扣本地日历库存,不走上游
|
||||
if (SmHotelDataSource.isLocal(hotel.getSheetName())) {
|
||||
if (localHotel) {
|
||||
return finishLocalOrder(order, product);
|
||||
}
|
||||
|
||||
@@ -226,7 +228,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return R.failed("酒店不存在,请传 hotelId 或 hotelCode");
|
||||
}
|
||||
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
|
||||
return R.failed("酒店已停用,无法下单");
|
||||
return R.failed("酒店已下架,无法下单");
|
||||
}
|
||||
String productCode = str(body.get("productCode"));
|
||||
if (StrUtil.isBlank(productCode)) {
|
||||
@@ -292,7 +294,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
guest.put("mobile", contactPhone);
|
||||
order.setGuestJson(JSONUtil.toJsonStr(List.of(guest)));
|
||||
}
|
||||
order.setMarkupPercent(priceConfigService.currentMarkupPercent());
|
||||
boolean localHotel = SmHotelDataSource.isLocal(hotel.getSheetName());
|
||||
order.setMarkupPercent(localHotel ? BigDecimal.ZERO : priceConfigService.currentMarkupPercent());
|
||||
if (product.getSellPrice() != null) {
|
||||
order.setSellAmount(product.getSellPrice().multiply(BigDecimal.valueOf(count)));
|
||||
}
|
||||
@@ -309,7 +312,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setReqJson(JSONUtil.toJsonStr(body));
|
||||
baseMapper.insert(order);
|
||||
|
||||
if (SmHotelDataSource.isLocal(hotel.getSheetName())) {
|
||||
if (localHotel) {
|
||||
R<JSONObject> local = finishLocalOrder(order, product);
|
||||
if (local.getData() != null) {
|
||||
local.getData().set("hotelName", hotel.getHotelName());
|
||||
|
||||
+4
-2
@@ -104,13 +104,15 @@ public class SmHotelPriceConfigServiceImpl extends ServiceImpl<SmHotelPriceConfi
|
||||
}
|
||||
int productUpdated = productMapper.resyncSellPriceByCost(rate);
|
||||
int dailyUpdated = dailyMapper.resyncSellPriceByCost(rate);
|
||||
log.info("|售价全局同步|percent={}|rate={}|productUpdated={}|dailyUpdated={}|", percent, rate, productUpdated,
|
||||
dailyUpdated);
|
||||
// 仅同步非本地(第三方)酒店;本地日历/产品卖价由手工维护,不受全局加价影响
|
||||
log.info("|售价全局同步(排除本地)|percent={}|rate={}|productUpdated={}|dailyUpdated={}|", percent, rate,
|
||||
productUpdated, dailyUpdated);
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("markupPercent", percent);
|
||||
out.put("rate", rate);
|
||||
out.put("productUpdated", productUpdated);
|
||||
out.put("dailyUpdated", dailyUpdated);
|
||||
out.put("scope", "non_local");
|
||||
return R.ok(out);
|
||||
}
|
||||
}
|
||||
+57
-5
@@ -114,8 +114,11 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SmHotelEntity saveHotel(SmHotelEntity entity) {
|
||||
normalize(entity);
|
||||
if (StrUtil.isBlank(entity.getHotelName()) && StrUtil.isBlank(entity.getHotelCode())) {
|
||||
throw new IllegalArgumentException("酒店名称与酒店编码不能同时为空");
|
||||
if (StrUtil.isBlank(entity.getHotelName())) {
|
||||
throw new IllegalArgumentException("酒店名称不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(entity.getPhone())) {
|
||||
throw new IllegalArgumentException("联系电话不能为空");
|
||||
}
|
||||
if (StrUtil.isNotBlank(entity.getHotelCode()) && existsHotelCode(entity.getHotelCode(), null)) {
|
||||
throw new IllegalArgumentException("酒店编码已存在:" + entity.getHotelCode());
|
||||
@@ -125,8 +128,10 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
entity.setSheetName(SmHotelDataSource.LOCAL);
|
||||
}
|
||||
if (StrUtil.isBlank(entity.getUpstreamStatus())) {
|
||||
entity.setUpstreamStatus("ACTIVE");
|
||||
// 本地新建默认下架,第三方导入默认上架
|
||||
entity.setUpstreamStatus(SmHotelDataSource.isLocal(entity.getSheetName()) ? "OFFLINE" : "ACTIVE");
|
||||
}
|
||||
normalizeLocalRoll(entity);
|
||||
if (entity.getDeptId() == null) {
|
||||
entity.setDeptId(SecurityUtils.getUser().getDeptId());
|
||||
}
|
||||
@@ -145,8 +150,11 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
throw new IllegalArgumentException("酒店不存在");
|
||||
}
|
||||
normalize(entity);
|
||||
if (StrUtil.isBlank(entity.getHotelName()) && StrUtil.isBlank(entity.getHotelCode())) {
|
||||
throw new IllegalArgumentException("酒店名称与酒店编码不能同时为空");
|
||||
if (StrUtil.isBlank(entity.getHotelName())) {
|
||||
throw new IllegalArgumentException("酒店名称不能为空");
|
||||
}
|
||||
if (SmHotelDataSource.isLocal(old.getSheetName()) && StrUtil.isBlank(entity.getPhone())) {
|
||||
throw new IllegalArgumentException("联系电话不能为空");
|
||||
}
|
||||
if (StrUtil.isNotBlank(entity.getHotelCode()) && existsHotelCode(entity.getHotelCode(), entity.getId())) {
|
||||
throw new IllegalArgumentException("酒店编码已存在:" + entity.getHotelCode());
|
||||
@@ -155,6 +163,29 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
if (StrUtil.isBlank(entity.getSheetName())) {
|
||||
entity.setSheetName(old.getSheetName());
|
||||
}
|
||||
if (SmHotelDataSource.isLocal(entity.getSheetName())) {
|
||||
if (StrUtil.isBlank(entity.getUpstreamStatus())) {
|
||||
entity.setUpstreamStatus(old.getUpstreamStatus());
|
||||
}
|
||||
else if (!"ACTIVE".equalsIgnoreCase(entity.getUpstreamStatus())
|
||||
&& !"OFFLINE".equalsIgnoreCase(entity.getUpstreamStatus())) {
|
||||
throw new IllegalArgumentException("上架状态仅支持上架或下架");
|
||||
}
|
||||
else {
|
||||
entity.setUpstreamStatus(entity.getUpstreamStatus().trim().toUpperCase());
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 第三方上架状态由上游差集维护,表单不允许改
|
||||
entity.setUpstreamStatus(old.getUpstreamStatus());
|
||||
}
|
||||
if (entity.getLocalAutoRoll() == null) {
|
||||
entity.setLocalAutoRoll(old.getLocalAutoRoll());
|
||||
}
|
||||
if (entity.getLocalRollHorizonDays() == null) {
|
||||
entity.setLocalRollHorizonDays(old.getLocalRollHorizonDays());
|
||||
}
|
||||
normalizeLocalRoll(entity);
|
||||
return baseMapper.updateById(entity) > 0;
|
||||
}
|
||||
|
||||
@@ -398,6 +429,27 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
entity.setIntro(trimToNull(entity.getIntro()));
|
||||
}
|
||||
|
||||
/** 本地滚加仅数据来源=local 有效;天数钳制 1~366 */
|
||||
private static void normalizeLocalRoll(SmHotelEntity entity) {
|
||||
if (!SmHotelDataSource.isLocal(entity.getSheetName())) {
|
||||
entity.setLocalAutoRoll(0);
|
||||
if (entity.getLocalRollHorizonDays() == null) {
|
||||
entity.setLocalRollHorizonDays(30);
|
||||
}
|
||||
return;
|
||||
}
|
||||
int on = entity.getLocalAutoRoll() != null && entity.getLocalAutoRoll() == 1 ? 1 : 0;
|
||||
entity.setLocalAutoRoll(on);
|
||||
int days = entity.getLocalRollHorizonDays() == null ? 30 : entity.getLocalRollHorizonDays();
|
||||
if (days < 1) {
|
||||
days = 1;
|
||||
}
|
||||
if (days > 366) {
|
||||
days = 366;
|
||||
}
|
||||
entity.setLocalRollHorizonDays(days);
|
||||
}
|
||||
|
||||
private static String trimToNull(String v) {
|
||||
return StrUtil.isBlank(v) ? null : v.trim();
|
||||
}
|
||||
|
||||
+171
-3
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelSyncLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
@@ -49,6 +50,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
private static final String TYPE_DETAIL = "DETAIL_PULL";
|
||||
|
||||
/** 本地酒店按配置补齐日历窗口 */
|
||||
private static final String TYPE_LOCAL_ROLL = "LOCAL_ROLL";
|
||||
|
||||
private static final String STATUS_RUNNING = "RUNNING";
|
||||
|
||||
private static final String STATUS_SUCCESS = "SUCCESS";
|
||||
@@ -140,6 +144,33 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return R.ok(logEntity, "已提交异步滚价");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<SmHotelSyncLogEntity> startLocalDailyRollAsync(String triggerType) {
|
||||
if (!properties.isLocalDailyRollEnabled() && !"MANUAL".equalsIgnoreCase(triggerType)) {
|
||||
return R.failed("本地自动滚加未启用");
|
||||
}
|
||||
abortStaleRunningSyncLogs();
|
||||
Long running = syncLogMapper.selectCount(Wrappers.<SmHotelSyncLogEntity>lambdaQuery()
|
||||
.eq(SmHotelSyncLogEntity::getSyncType, TYPE_LOCAL_ROLL)
|
||||
.eq(SmHotelSyncLogEntity::getStatus, STATUS_RUNNING));
|
||||
if (running != null && running > 0) {
|
||||
return R.failed("已有本地滚加任务进行中(若长时间卡住请在同步日志点「终止」)");
|
||||
}
|
||||
SmHotelSyncLogEntity logEntity = newLog(TYPE_LOCAL_ROLL, triggerType);
|
||||
logEntity.setStage("QUEUED");
|
||||
logEntity.setStageMsg("已提交本地滚加任务…");
|
||||
logEntity.setProgressPct(1);
|
||||
syncLogMapper.insert(logEntity);
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
}
|
||||
final Long tid = tenantId;
|
||||
final Long logId = logEntity.getId();
|
||||
EXECUTOR.execute(() -> TenantBroker.runAs(tid, id -> runLocalDailyRoll(logId)));
|
||||
return R.ok(logEntity, "已提交本地滚加");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<SmHotelSyncLogEntity> startDetailPullAsync(Integer limit, String triggerType) {
|
||||
abortStaleRunningSyncLogs();
|
||||
@@ -402,10 +433,14 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
int skip = 0;
|
||||
int processed = 0;
|
||||
List<String> failLines = new ArrayList<>();
|
||||
// 上游滚价排除本地数据来源酒店
|
||||
Long totalHint = hotelMapper.selectCount(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE")));
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
|
||||
.and(w -> w.isNull(SmHotelEntity::getSheetName)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getSheetName, SmHotelDataSource.LOCAL)));
|
||||
long total = totalHint == null ? 0L : totalHint;
|
||||
|
||||
while (true) {
|
||||
@@ -416,6 +451,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
|
||||
.and(w -> w.isNull(SmHotelEntity::getSheetName)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getSheetName, SmHotelDataSource.LOCAL))
|
||||
.orderByAsc(SmHotelEntity::getId)
|
||||
.last("limit " + batchSize));
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
@@ -485,6 +523,114 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地酒店自动滚加:对开启配置的酒店,按产品默认价补齐「今天起共 N 天」缺失日历行。
|
||||
*/
|
||||
private void runLocalDailyRoll(Long logId) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
progress(logId, 5, "ROLL", "开始本地滚加(补齐缺失日历)…");
|
||||
int batchSize = Math.max(20, properties.getPriceRollBatchSize());
|
||||
long lastId = 0L;
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
int skip = 0;
|
||||
int insertedTotal = 0;
|
||||
int processed = 0;
|
||||
List<String> failLines = new ArrayList<>();
|
||||
Long totalHint = hotelMapper.selectCount(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.eq(SmHotelEntity::getSheetName, SmHotelDataSource.LOCAL)
|
||||
.eq(SmHotelEntity::getLocalAutoRoll, 1)
|
||||
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE")));
|
||||
long total = totalHint == null ? 0L : totalHint;
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
while (true) {
|
||||
List<SmHotelEntity> rows = hotelMapper.selectList(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.select(SmHotelEntity::getId, SmHotelEntity::getHotelCode, SmHotelEntity::getHotelName,
|
||||
SmHotelEntity::getLocalRollHorizonDays)
|
||||
.gt(SmHotelEntity::getId, lastId)
|
||||
.eq(SmHotelEntity::getSheetName, SmHotelDataSource.LOCAL)
|
||||
.eq(SmHotelEntity::getLocalAutoRoll, 1)
|
||||
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
|
||||
.orderByAsc(SmHotelEntity::getId)
|
||||
.last("limit " + batchSize));
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
break;
|
||||
}
|
||||
for (SmHotelEntity h : rows) {
|
||||
lastId = h.getId();
|
||||
processed++;
|
||||
int horizon = h.getLocalRollHorizonDays() == null ? 30 : h.getLocalRollHorizonDays();
|
||||
if (horizon < 1) {
|
||||
horizon = 1;
|
||||
}
|
||||
if (horizon > 366) {
|
||||
horizon = 366;
|
||||
}
|
||||
try {
|
||||
SmHotelLocalDailyBatchDTO req = new SmHotelLocalDailyBatchDTO();
|
||||
req.setMode("FILL");
|
||||
req.setStartDate(today);
|
||||
req.setEndDate(today.plusDays(horizon - 1L));
|
||||
req.setDailyStatus(1);
|
||||
R<Map<String, Object>> r = midService.batchLocalDaily(h.getId(), req);
|
||||
if (r != null && r.getCode() == 0) {
|
||||
ok++;
|
||||
Object inserted = r.getData() == null ? null : r.getData().get("inserted");
|
||||
if (inserted instanceof Number) {
|
||||
insertedTotal += ((Number) inserted).intValue();
|
||||
}
|
||||
}
|
||||
else {
|
||||
fail++;
|
||||
appendFail(failLines, h, r == null ? "返回为空" : StrUtil.blankToDefault(r.getMsg(), "业务失败"));
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
fail++;
|
||||
appendFail(failLines, h, e.getMessage());
|
||||
}
|
||||
}
|
||||
int pct = total <= 0 ? 50 : Math.min(95, 5 + (int) (processed * 90L / total));
|
||||
progress(logId, pct, "ROLL",
|
||||
"本地滚加中 已处理 " + processed + "/" + total + " 成功 " + ok + " 失败 " + fail
|
||||
+ " 新补 " + insertedTotal);
|
||||
if (rows.size() < batchSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SmHotelSyncLogEntity done = syncLogMapper.selectById(logId);
|
||||
if (done == null) {
|
||||
return;
|
||||
}
|
||||
done.setStatus(STATUS_SUCCESS);
|
||||
done.setStage("DONE");
|
||||
done.setStageMsg("本地滚加完成,新补日历行 " + insertedTotal);
|
||||
done.setProgressPct(100);
|
||||
done.setSuccessCount(ok);
|
||||
done.setFailCount(fail);
|
||||
done.setSkipCount(skip);
|
||||
done.setLocalCount(total);
|
||||
done.setNewCount((long) insertedTotal);
|
||||
applyFailDetail(done, failLines, fail);
|
||||
done.setCostMs(System.currentTimeMillis() - t0);
|
||||
done.setFinishTime(LocalDateTime.now());
|
||||
done.setUpdateTime(LocalDateTime.now());
|
||||
syncLogMapper.updateById(done);
|
||||
log.info("|本地滚加|完成|logId={}|ok={}|fail={}|inserted={}|", logId, ok, fail, insertedTotal);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
log.error("|本地滚加|失败|logId={}|", logId, e);
|
||||
failLog(logId, e.getMessage(), System.currentTimeMillis() - t0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对本地全部启用酒店逐家 pullFromUpstream(综合信息 + 未来 price-days 房价)。
|
||||
*/
|
||||
@@ -1029,10 +1175,12 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
m.put("hotelListDailyCron", properties.getHotelListDailyCron());
|
||||
m.put("priceRollDailyEnabled", properties.isPriceRollDailyEnabled());
|
||||
m.put("priceRollDailyCron", properties.getPriceRollDailyCron());
|
||||
m.put("localDailyRollEnabled", properties.isLocalDailyRollEnabled());
|
||||
m.put("localDailyRollCron", properties.getLocalDailyRollCron());
|
||||
m.put("autoImportNewHotels", properties.isAutoImportNewHotels());
|
||||
m.put("autoMarkOfflineHotels", properties.isAutoMarkOfflineHotels());
|
||||
m.put("hint", "改 Nacos/本地 shangyou-hotel.hotel-list-daily-cron、price-roll-daily-cron;"
|
||||
+ "若用 XXL 请设 spring-schedule-enabled=false,在 XXL 控制台配 Cron");
|
||||
m.put("hint", "改 Nacos/本地 shangyou-hotel.hotel-list-daily-cron、price-roll-daily-cron、local-daily-roll-cron;"
|
||||
+ "若用 XXL 请设 spring-schedule-enabled=false,在 XXL 控制台配 Cron(含 smHotelLocalDailyRollJobHandler)");
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -1076,4 +1224,24 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return Map.of("ok", true, "async", true, "logId", r.getData().getId());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> jobRollLocalDaily() {
|
||||
return jobRollLocalDaily("XXL");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> jobRollLocalDaily(String triggerType) {
|
||||
Long tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
return TenantBroker.applyAs(tenantId, tid -> {
|
||||
if (!properties.isLocalDailyRollEnabled()) {
|
||||
return Map.of("skipped", true, "reason", "local-daily-roll-enabled=false");
|
||||
}
|
||||
R<SmHotelSyncLogEntity> r = startLocalDailyRollAsync(StrUtil.blankToDefault(triggerType, "XXL"));
|
||||
if (r.getCode() != 0) {
|
||||
return Map.of("ok", false, "msg", r.getMsg());
|
||||
}
|
||||
return Map.of("ok", true, "async", true, "logId", r.getData().getId());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user