酒店增加本地维护

This commit is contained in:
zjf committed 2026-10-09 15:39:21 +08:00
1 parent 80b4d1e9cc
commit e2ae48d0e5
17 files changed
+630 -60

No files matched your search

@@ -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;
@@ -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;
@@ -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);
}
@@ -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) {
@@ -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);
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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());
}
}
}
@@ -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);
@@ -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);
@@ -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);
}
@@ -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);
}
@@ -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;
}
@@ -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());
@@ -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);
}
}
@@ -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();
}
@@ -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());
});
}
}