Merge branch 'master' of https://gitea.zianxin.com/zax/biz-api-cloud-zax
This commit is contained in:
commit
b4fa60a895
32 files changed
+1186
-241
No files matched your search
+29
-24
@@ -7,11 +7,9 @@ import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 每日菜谱导入/导出 Excel 行(导出含菜谱 ID,导入时可按 ID 更新)
|
||||
* <p>列顺序:日期、餐段、套餐名、菜品名、分类、单价、套餐价、线上库存、线下库存、配料、菜谱ID、菜品SPU ID、子SPU ID、填写说明
|
||||
* <p>线上/线下库存挂在每日菜谱行上,不再写入菜品列表 {@code canteen_spu.stock}。
|
||||
* <p>列顺序:日期、餐段、套餐名/别名、类型、菜品名、分类、单价、套餐价、总库存、移动端可预定数量、配料、菜谱ID、菜品SPU ID、子SPU ID、填写说明
|
||||
* <p>库存:总库存→{@code offline_stock};移动端可预定数量→{@code online_stock};消费机可售=总库存−移动端已预订。
|
||||
* <p>菜谱ID / 菜品SPU ID / 子SPU ID 必须用文本导出(带制表符前缀),否则 Excel 对超长雪花 ID 会丢精度。
|
||||
* <p>业务列与 ID 列导出时每行写全,均不合并。
|
||||
* <p>无「菜品SPU ID」时一律新建菜品/套餐;无「子SPU ID」时组成菜按菜名+配料回退或新建。
|
||||
*/
|
||||
@Data
|
||||
public class CanteenDailyMenuImportExcel {
|
||||
@@ -22,63 +20,70 @@ public class CanteenDailyMenuImportExcel {
|
||||
@ExcelProperty(value = "餐段", index = 1)
|
||||
private String mealName;
|
||||
|
||||
@ExcelProperty(value = "套餐名", index = 2)
|
||||
/**
|
||||
* 套餐:同一套餐标签(多行组成菜共用);单品:填「无」或本排菜别名。
|
||||
*/
|
||||
@ExcelProperty(value = "套餐名/别名", index = 2)
|
||||
private String comboName;
|
||||
|
||||
@ExcelProperty(value = "菜品名", index = 3)
|
||||
/**
|
||||
* 排菜类型:套餐 / 单品。留空则自动识别。
|
||||
*/
|
||||
@ExcelProperty(value = "类型", index = 3)
|
||||
private String menuType;
|
||||
|
||||
@ExcelProperty(value = "菜品名", index = 4)
|
||||
private String dishName;
|
||||
|
||||
/** 分类名称;新建菜品时优先按此匹配,匹配不到再用默认分类 */
|
||||
@ExcelProperty(value = "分类", index = 4)
|
||||
@ExcelProperty(value = "分类", index = 5)
|
||||
private String categoryName;
|
||||
|
||||
/** 菜品自身单价;不填默认 0 */
|
||||
@ExcelProperty(value = "单价(元)", index = 5)
|
||||
@ExcelProperty(value = "单价(元)", index = 6)
|
||||
private BigDecimal dishPriceYuan;
|
||||
|
||||
/** 套餐售价(套餐首行填写);单品可留空 */
|
||||
@ExcelProperty(value = "套餐价(元)", index = 6)
|
||||
@ExcelProperty(value = "套餐价(元)", index = 7)
|
||||
private BigDecimal priceYuan;
|
||||
|
||||
/**
|
||||
* 当日线上可预约库存(挂每日菜谱)。单品填本行;套餐组成行建议每行都填相同值。
|
||||
* 不填:默认 600。
|
||||
* 总库存(入库 {@code offline_stock})。单品填本行;套餐组成行建议每行同值。不填默认 600。
|
||||
*/
|
||||
@ExcelProperty(value = "线上库存", index = 7)
|
||||
private Integer onlineStock;
|
||||
|
||||
/**
|
||||
* 当日线下销售库存(挂每日菜谱,安卓消费机读此池)。单品填本行;套餐组成行建议每行都填相同值。
|
||||
* 不填:默认 600。
|
||||
*/
|
||||
@ExcelProperty(value = "线下库存", index = 8)
|
||||
@ExcelProperty(value = "总库存", index = 8)
|
||||
private Integer offlineStock;
|
||||
|
||||
@ExcelProperty(value = "配料", index = 9)
|
||||
/**
|
||||
* 移动端可预定数量上限(入库 {@code online_stock})。不填默认 600。
|
||||
*/
|
||||
@ExcelProperty(value = "移动端可预定数量", index = 9)
|
||||
private Integer onlineStock;
|
||||
|
||||
@ExcelProperty(value = "配料", index = 10)
|
||||
private String ingredients;
|
||||
|
||||
/**
|
||||
* 每日菜谱记录 ID(文本)。导入时有则更新,无则新增。
|
||||
* 勿改成数字格式,否则 Excel 会丢精度。
|
||||
*/
|
||||
@ExcelProperty(value = "菜谱ID", index = 10, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class)
|
||||
@ExcelProperty(value = "菜谱ID", index = 11, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 关联菜品 SPU ID(文本)。套餐块各行填同一套餐本体 ID(导出可合并单元格)。
|
||||
* 单品行=该单品 ID。导入:有则按 ID 更新;无则新建。
|
||||
*/
|
||||
@ExcelProperty(value = "菜品SPU ID", index = 11, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class)
|
||||
@ExcelProperty(value = "菜品SPU ID", index = 12, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class)
|
||||
private String spuId;
|
||||
|
||||
/**
|
||||
* 套餐组成菜 SPU ID(文本,每行一个,不合并)。
|
||||
* 有则按 ID 更新/复用组成菜;无则按「菜名+配料」回退或新建。单品行可留空。
|
||||
*/
|
||||
@ExcelProperty(value = "子SPU ID", index = 12, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class)
|
||||
@ExcelProperty(value = "子SPU ID", index = 13, converter = com.bsd.pjcloud.canteen.excel.ExcelSnowflakeIdConverter.class)
|
||||
private String childSpuId;
|
||||
|
||||
/** 仅示例说明用,导入逻辑忽略 */
|
||||
@ExcelProperty(value = "填写说明", index = 13)
|
||||
@ExcelProperty(value = "填写说明", index = 14)
|
||||
private String tip;
|
||||
}
|
||||
+2
-2
@@ -34,13 +34,13 @@ public class CanteenSpuUpdateStockReqDTO {
|
||||
|
||||
/**
|
||||
* 每日菜谱分池:ONLINE=线上可预约;OFFLINE=线下消费机。
|
||||
* 空则仍扣 {@code canteen_spu.stock}(兼容非每日菜谱)。
|
||||
* 空则默认 OFFLINE(兼容未传通道的安卓消费机),扣每日菜谱线下销量。
|
||||
*/
|
||||
@Pattern(regexp = "ONLINE|OFFLINE", message = "库存通道错误")
|
||||
private String stockChannel;
|
||||
|
||||
/**
|
||||
* 每日菜谱所属商户;stockChannel 非空时必填。
|
||||
* 每日菜谱所属商户;分池扣减时建议传,空则从 SPU 回填。
|
||||
*/
|
||||
private Long merchantId;
|
||||
|
||||
|
||||
+2
-2
@@ -54,9 +54,9 @@ public class CanteenCategoryEntity extends Model<CanteenCategoryEntity> {
|
||||
private String picUrl;
|
||||
|
||||
/**
|
||||
* 分类排序
|
||||
* 分类序号(越大越靠前)
|
||||
*/
|
||||
@Schema(description="分类排序")
|
||||
@Schema(description="分类序号,越大越靠前")
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
|
||||
+10
-4
@@ -43,6 +43,12 @@ public class CanteenDailyMenuEntity extends Model<CanteenDailyMenuEntity> {
|
||||
@Schema(description="商品 SPU 编号")
|
||||
private Long spuId;
|
||||
|
||||
/**
|
||||
* 本排菜展示名:套餐名或单品别名(仅挂菜谱行,不写回菜品)
|
||||
*/
|
||||
@Schema(description = "套餐名或单品别名")
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* 归属时间 1早餐 2午餐 3晚餐
|
||||
*/
|
||||
@@ -50,15 +56,15 @@ public class CanteenDailyMenuEntity extends Model<CanteenDailyMenuEntity> {
|
||||
private Integer hourType;
|
||||
|
||||
/**
|
||||
* 线上可预约库存配额(人工设置;下单不改;剩余=配额−onlineSold)
|
||||
* 移动端可预订数量上限(人工;下单不改)
|
||||
*/
|
||||
@Schema(description = "线上可预约库存配额")
|
||||
@Schema(description = "移动端可预订数量上限")
|
||||
private Integer onlineStock;
|
||||
|
||||
/**
|
||||
* 线下销售库存配额(人工设置;下单不改;剩余=配额−offlineSold)
|
||||
* 总库存(人工;下单不改)。消费机可售 = 总库存 − 移动端已预订。
|
||||
*/
|
||||
@Schema(description = "线下销售库存配额")
|
||||
@Schema(description = "总库存")
|
||||
private Integer offlineStock;
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ public class CanteenSpuEntity extends Model<CanteenSpuEntity> {
|
||||
private String nameEn;
|
||||
|
||||
/**
|
||||
* 套餐名(仅 spuType=2 时使用)
|
||||
* 套餐名(仅 spuType=2 套餐本体;每日排菜展示名见 canteen_daily_menu.display_name)
|
||||
*/
|
||||
@Schema(description = "套餐名")
|
||||
private String comboName;
|
||||
|
||||
+6
-3
@@ -44,7 +44,7 @@ public class CanteenDailyMenuImportPreviewVO {
|
||||
@Schema(description = "餐段")
|
||||
private String mealName;
|
||||
|
||||
@Schema(description = "套餐名")
|
||||
@Schema(description = "套餐名/别名")
|
||||
private String comboName;
|
||||
|
||||
@Schema(description = "菜品名")
|
||||
@@ -59,10 +59,10 @@ public class CanteenDailyMenuImportPreviewVO {
|
||||
@Schema(description = "套餐价(元)")
|
||||
private BigDecimal priceYuan;
|
||||
|
||||
@Schema(description = "线上库存(每日)")
|
||||
@Schema(description = "移动端可预定数量")
|
||||
private Integer onlineStock;
|
||||
|
||||
@Schema(description = "线下库存(每日)")
|
||||
@Schema(description = "总库存")
|
||||
private Integer offlineStock;
|
||||
|
||||
@Schema(description = "配料")
|
||||
@@ -77,6 +77,9 @@ public class CanteenDailyMenuImportPreviewVO {
|
||||
@Schema(description = "子SPU ID(套餐组成菜)")
|
||||
private String childSpuId;
|
||||
|
||||
@Schema(description = "类型:套餐/单品,空则自动识别")
|
||||
private String menuType;
|
||||
|
||||
/**
|
||||
* 排菜分组键:同一套餐多行组成菜共用,前端按此统计「一道菜」而不是 Excel 行数。
|
||||
*/
|
||||
|
||||
+18
-8
@@ -27,9 +27,14 @@ public class DailyMenuAdminVO {
|
||||
|
||||
private String name;
|
||||
|
||||
/** 套餐名(canteen_spu.combo_name),导出 Excel「套餐名」列优先 */
|
||||
/**
|
||||
* 展示名:优先排菜 display_name,空则回落菜品 combo_name(列表/App 展示)
|
||||
*/
|
||||
private String comboName;
|
||||
|
||||
/** 排菜 display_name 原值(导出单品「套餐名/别名」列用,不含菜品回落) */
|
||||
private String displayName;
|
||||
|
||||
/** 套餐标签兜底(canteen_spu.introduction),combo_name 为空时导出用 */
|
||||
private String introduction;
|
||||
|
||||
@@ -44,31 +49,36 @@ public class DailyMenuAdminVO {
|
||||
|
||||
private Integer sort;
|
||||
|
||||
/** 线下库存配额(人工) */
|
||||
/**
|
||||
* 编辑提交时传「总库存」写入库 {@code offline_stock}(列表不再单独展示消费机可销售数量)。
|
||||
*/
|
||||
private Integer offlineStock;
|
||||
|
||||
/** 线上可预约库存配额(人工) */
|
||||
/** 移动端可预订数量上限(库 {@code online_stock}) */
|
||||
private Integer onlineStock;
|
||||
|
||||
/** 总库存(库 {@code offline_stock}) */
|
||||
private Integer totalStock;
|
||||
|
||||
/**
|
||||
* 兼容旧字段:对安卓/列表默认展示「线下剩余」。
|
||||
* 管理端同时看 remainOnlineStock / remainOfflineStock。
|
||||
*/
|
||||
private Integer stock;
|
||||
|
||||
/** 线上净销量(仅服务端填剩余用,可不展示) */
|
||||
/** 移动端已订数量(管理端列表展示) */
|
||||
private Integer onlineSold;
|
||||
|
||||
/** 线下净销量(仅服务端填剩余用,可不展示) */
|
||||
/** 消费机已售净销量(管理端列表展示) */
|
||||
private Integer offlineSold;
|
||||
|
||||
/** 线上剩余 = onlineStock − 预约净销量 */
|
||||
/** 移动端可预订剩余(服务端计算用,管理端列表改展示 onlineSold) */
|
||||
private Integer remainOnlineStock;
|
||||
|
||||
/** 线下剩余 = offlineStock − 线下净销量 */
|
||||
/** 剩余总库存(与 remainStock 同义,兼容字段) */
|
||||
private Integer remainOfflineStock;
|
||||
|
||||
/** 剩余合计(线上剩余+线下剩余),兼容旧列 remainStock */
|
||||
/** 剩余总库存 */
|
||||
private Integer remainStock;
|
||||
|
||||
private Integer price;
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ public class DailyMenuVO {
|
||||
private Integer spuType;
|
||||
|
||||
/**
|
||||
* 套餐名(canteen_spu.combo_name)
|
||||
* 套餐名或单品别名(canteen_spu.combo_name)
|
||||
*/
|
||||
private String comboName;
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public class SpuPrintNameVo {
|
||||
private Long productId;
|
||||
|
||||
/**
|
||||
* 套餐名(仅 spuType=2 套餐有值),打印凭证优先展示。
|
||||
* 套餐名(canteen_spu.combo_name);每日排菜别名见 display_name,打印若需排菜名需另查菜谱。
|
||||
*/
|
||||
@Schema(description = "套餐名")
|
||||
private String comboName;
|
||||
|
||||
+7
-7
@@ -93,26 +93,26 @@ public class DailyMenuAppVO {
|
||||
|
||||
/**
|
||||
* 当前可售库存。
|
||||
* <p>App/移动端:线上剩余;安卓消费机 AD 接口:线下剩余。字段名保持 stock 兼容旧客户端。
|
||||
* <p>App/移动端:移动端可预订剩余;AD:总库存 − 移动端已预订。字段名保持 stock 兼容旧客户端。
|
||||
*/
|
||||
private Integer stock;
|
||||
|
||||
/** 线上配额(人工;下单不改) */
|
||||
/** 移动端可预订上限(库 online_stock) */
|
||||
private Integer onlineStock;
|
||||
|
||||
/** 线下配额(人工;下单不改) */
|
||||
/** 总库存(库 offline_stock) */
|
||||
private Integer offlineStock;
|
||||
|
||||
/** 线上净销量(服务端计算剩余用) */
|
||||
/** 移动端已预订净销量 */
|
||||
private Integer onlineSold;
|
||||
|
||||
/** 线下净销量(服务端计算剩余用) */
|
||||
/** 消费机已售净销量 */
|
||||
private Integer offlineSold;
|
||||
|
||||
/** 线上剩余 = onlineStock − onlineSold */
|
||||
/** 移动端可预订剩余 */
|
||||
private Integer remainOnlineStock;
|
||||
|
||||
/** 线下剩余 = offlineStock − offlineSold */
|
||||
/** App:剩余总库存;AD:与 stock 同为 总库存−移动端已预订 */
|
||||
private Integer remainOfflineStock;
|
||||
|
||||
/**
|
||||
|
||||
+8
-2
@@ -55,6 +55,8 @@ public class CanteenCategoryController {
|
||||
LambdaQueryWrapper<CanteenCategoryEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.like(StrUtil.isNotBlank(canteenCategory.getName()), CanteenCategoryEntity::getName, canteenCategory.getName());
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenCategory.getMerchantId(), CanteenCategoryEntity::getMerchantId);
|
||||
// 分类序号越大越靠前
|
||||
wrapper.orderByDesc(CanteenCategoryEntity::getSort).orderByDesc(CanteenCategoryEntity::getId);
|
||||
return R.ok(canteenCategoryService.selectPageByScope(page, wrapper));
|
||||
}
|
||||
|
||||
@@ -136,7 +138,10 @@ public class CanteenCategoryController {
|
||||
@GetMapping("/export")
|
||||
@PreAuthorize("@pms.hasPermission('canteen_canteenCategory_export')")
|
||||
public List<CanteenCategoryEntity> export(CanteenCategoryEntity canteenCategory, Long[] ids) {
|
||||
return canteenCategoryService.list(Wrappers.lambdaQuery(canteenCategory).in(ArrayUtil.isNotEmpty(ids), CanteenCategoryEntity::getId, ids));
|
||||
return canteenCategoryService.list(Wrappers.lambdaQuery(canteenCategory)
|
||||
.in(ArrayUtil.isNotEmpty(ids), CanteenCategoryEntity::getId, ids)
|
||||
.orderByDesc(CanteenCategoryEntity::getSort)
|
||||
.orderByDesc(CanteenCategoryEntity::getId));
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +164,8 @@ public class CanteenCategoryController {
|
||||
LambdaQueryWrapper<CanteenCategoryEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.like(StringUtils.isNotBlank(name), CanteenCategoryEntity::getName, name);
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, merchantId, CanteenCategoryEntity::getMerchantId);
|
||||
wrapper.orderByAsc(CanteenCategoryEntity::getSort);
|
||||
// 分类序号越大越靠前
|
||||
wrapper.orderByDesc(CanteenCategoryEntity::getSort).orderByDesc(CanteenCategoryEntity::getId);
|
||||
wrapper.apply(StringUtils.isNotEmpty(categoryType), "FIND_IN_SET({0}, category_type)", categoryType);
|
||||
List<CanteenCategoryEntity> list = canteenCategoryService.listScope(wrapper);
|
||||
return R.ok(list);
|
||||
|
||||
+4
-1
@@ -56,6 +56,8 @@ public class CanteenOnlineCategoryController {
|
||||
LambdaQueryWrapper<CanteenOnlineCategoryEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.like(StrUtil.isNotBlank(canteenOnlineCategory.getName()),CanteenOnlineCategoryEntity::getName,canteenOnlineCategory.getName());
|
||||
merchantScopeHelper.applyMerchantIdScope(wrapper, canteenOnlineCategory.getMerchantId(), CanteenOnlineCategoryEntity::getMerchantId);
|
||||
// 分类序号越大越靠前
|
||||
wrapper.orderByDesc(CanteenOnlineCategoryEntity::getSort).orderByDesc(CanteenOnlineCategoryEntity::getId);
|
||||
return R.ok(canteenOnlineCategoryService.pageScope(page, wrapper));
|
||||
}
|
||||
|
||||
@@ -153,7 +155,8 @@ public class CanteenOnlineCategoryController {
|
||||
@GetMapping("/app/getAllSimple")
|
||||
public R getAllSimple(@RequestParam(value = "merchantId")Long merchantId) {
|
||||
LambdaQueryWrapper<CanteenOnlineCategoryEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.orderByAsc(CanteenOnlineCategoryEntity::getSort);
|
||||
// 分类序号越大越靠前
|
||||
wrapper.orderByDesc(CanteenOnlineCategoryEntity::getSort).orderByDesc(CanteenOnlineCategoryEntity::getId);
|
||||
wrapper.eq(CanteenOnlineCategoryEntity::getMerchantId,merchantId);
|
||||
// wrapper.apply(StringUtils.isNotEmpty(categoryType),"FIND_IN_SET({0}, your_column_name)",categoryType);
|
||||
List<CanteenOnlineCategoryEntity> list = canteenOnlineCategoryService.list(wrapper);
|
||||
|
||||
+21
-18
@@ -12,8 +12,8 @@ import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||
|
||||
/**
|
||||
* 每日菜谱导入模板 / 导出写后处理。
|
||||
* <p>列:0日期 1餐段 2套餐名 3菜品名 4分类 5单价 6套餐价 7线上库存 8线下库存 9配料
|
||||
* 10菜谱ID 11菜品SPU ID 12子SPU ID 13填写说明
|
||||
* <p>列:0日期 1餐段 2套餐名/别名 3类型 4菜品名 5分类 6单价 7套餐价 8总库存 9移动端可预定数量 10配料
|
||||
* 11菜谱ID 12菜品SPU ID 13子SPU ID 14填写说明
|
||||
* <p>业务列每行写全、不合并。雪花 ID 三列也<strong>每行写全、不合并</strong>
|
||||
* (合并会导致 EasyExcel 读空;Excel 把 19 位 ID 当数字会丢精度并串号)。
|
||||
* <p>须配合 {@code @ResponseExcel(inMemory = true)},否则 SXSSF 刷盘后改单元格会失败。
|
||||
@@ -23,12 +23,14 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
private static final int COL_DATE = 0;
|
||||
private static final int COL_MEAL = 1;
|
||||
private static final int COL_COMBO = 2;
|
||||
private static final int COL_COMBO_PRICE = 6;
|
||||
private static final int COL_ONLINE_STOCK = 7;
|
||||
private static final int COL_OFFLINE_STOCK = 8;
|
||||
private static final int COL_ID = 10;
|
||||
private static final int COL_SPU = 11;
|
||||
private static final int COL_CHILD_SPU = 12;
|
||||
private static final int COL_COMBO_PRICE = 7;
|
||||
/** 总库存(Excel index 8 → offlineStock 字段) */
|
||||
private static final int COL_TOTAL_STOCK = 8;
|
||||
/** 移动端可预定数量(Excel index 9 → onlineStock 字段) */
|
||||
private static final int COL_MOBILE_QUOTA = 9;
|
||||
private static final int COL_ID = 11;
|
||||
private static final int COL_SPU = 12;
|
||||
private static final int COL_CHILD_SPU = 13;
|
||||
|
||||
/** 写入 ID 前缀,迫使 Excel/WPS 保持文本,避免 19 位雪花被当数字丢精度 */
|
||||
public static final String ID_TEXT_PREFIX = "\t";
|
||||
@@ -82,8 +84,8 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
String spuId = cellText(sheet, r, COL_SPU);
|
||||
String childSpuId = cellText(sheet, r, COL_CHILD_SPU);
|
||||
String comboPrice = cellText(sheet, r, COL_COMBO_PRICE);
|
||||
String online = cellText(sheet, r, COL_ONLINE_STOCK);
|
||||
String offline = cellText(sheet, r, COL_OFFLINE_STOCK);
|
||||
String online = cellText(sheet, r, COL_TOTAL_STOCK);
|
||||
String offline = cellText(sheet, r, COL_MOBILE_QUOTA);
|
||||
|
||||
if (isBlank(date)) {
|
||||
date = lastDate;
|
||||
@@ -194,8 +196,8 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
writeText(sheet, r, COL_COMBO, combo, center);
|
||||
if (inCombo && !isSingleCombo(combo)) {
|
||||
writeText(sheet, r, COL_COMBO_PRICE, comboPrice, center);
|
||||
writeText(sheet, r, COL_ONLINE_STOCK, online, center);
|
||||
writeText(sheet, r, COL_OFFLINE_STOCK, offline, center);
|
||||
writeText(sheet, r, COL_TOTAL_STOCK, online, center);
|
||||
writeText(sheet, r, COL_MOBILE_QUOTA, offline, center);
|
||||
}
|
||||
writeIdText(sheet, r, COL_ID, menuId, idTextStyle);
|
||||
writeIdText(sheet, r, COL_SPU, spuId, idTextStyle);
|
||||
@@ -332,17 +334,18 @@ public class CanteenDailyMenuTemplateMergeHandler implements WorkbookWriteHandle
|
||||
private static void setColumnWidths(Sheet sheet) {
|
||||
sheet.setColumnWidth(0, 12 * 256);
|
||||
sheet.setColumnWidth(1, 8 * 256);
|
||||
sheet.setColumnWidth(2, 10 * 256);
|
||||
sheet.setColumnWidth(3, 16 * 256);
|
||||
sheet.setColumnWidth(4, 12 * 256);
|
||||
sheet.setColumnWidth(2, 12 * 256);
|
||||
sheet.setColumnWidth(3, 8 * 256);
|
||||
sheet.setColumnWidth(4, 16 * 256);
|
||||
sheet.setColumnWidth(5, 12 * 256);
|
||||
sheet.setColumnWidth(6, 12 * 256);
|
||||
sheet.setColumnWidth(7, 10 * 256);
|
||||
sheet.setColumnWidth(7, 12 * 256);
|
||||
sheet.setColumnWidth(8, 10 * 256);
|
||||
sheet.setColumnWidth(9, 22 * 256);
|
||||
sheet.setColumnWidth(9, 10 * 256);
|
||||
sheet.setColumnWidth(10, 22 * 256);
|
||||
sheet.setColumnWidth(11, 22 * 256);
|
||||
sheet.setColumnWidth(12, 22 * 256);
|
||||
sheet.setColumnWidth(13, 48 * 256);
|
||||
sheet.setColumnWidth(13, 22 * 256);
|
||||
sheet.setColumnWidth(14, 48 * 256);
|
||||
}
|
||||
}
|
||||
+182
-48
@@ -97,26 +97,28 @@ public class CanteenDailyMenuImportHelper {
|
||||
public List<CanteenDailyMenuImportExcel> templateRows() {
|
||||
List<CanteenDailyMenuImportExcel> list = new ArrayList<>();
|
||||
// 业务列与雪花 ID 均每行写全(文本),避免 Excel 数字精度串号
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "80", "100",
|
||||
"套餐多行:日期/餐段/套餐名/套餐价/库存/菜谱ID 每行都填;菜品SPU ID=套餐本体;子SPU ID=本行组成菜;ID列必须为文本"));
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "蒜苔炒腊肉", "荤菜", null, "8", "21", "80", "100", null));
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "清炒时蔬", "素菜", null, "3", "21", "80", "100", null));
|
||||
list.add(row("2026-08-17", "午餐", "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "60", "80",
|
||||
"换套餐名即另一套;无菜谱ID时新建;有菜谱ID/套餐SPU则更新;打开Excel后勿把ID列改成数字"));
|
||||
list.add(row("2026-08-17", "午餐", "B套餐", "土豆丝", "素菜", null, "4", "18", "60", "80", null));
|
||||
list.add(row("2026-08-17", "午餐", "无", "千张大骨汤", "汤类", null, "0", null, "50", "50",
|
||||
"单品:套餐名填「无」;菜品SPU ID=本行单品;子SPU ID可空"));
|
||||
list.add(row("2026-08-17", "早餐", "单品-1", "榨菜肉丝面", "面食", "榨菜,肉丝", "6.4", null, "30", "40",
|
||||
"单品-xxx / 单点-xxx 一律按单品,套餐价可留空;不填库存则默认600"));
|
||||
list.add(row("2026-08-17", "早餐", "单点-2", "豆浆", "饮品", null, "2", null, "100", "100", null));
|
||||
list.add(row("2026-08-18", "午餐,晚餐", "无", "番茄蛋汤", "汤类", null, "2", null, "80", "80",
|
||||
"多餐拆行:无菜品SPU ID时各餐段各自新建菜品;每日线上/线下库存各自独立"));
|
||||
// 库存列:总库存、移动端可预定数量(消费机可售=总库存−移动端已预订,不单独导入)
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "糖醋排骨", "荤菜", "青莲仔排", "12", "21", "100", "80", "套餐",
|
||||
"总库存+移动端可预定;类型填套餐/单品;菜品SPU ID=套餐本体;子SPU ID=组成菜"));
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "蒜苔炒腊肉", "荤菜", null, "8", "21", "100", "80", "套餐", null));
|
||||
list.add(row("2026-08-17", "午餐", "A套餐", "清炒时蔬", "素菜", null, "3", "21", "100", "80", "套餐", null));
|
||||
list.add(row("2026-08-17", "午餐", "B套餐", "红烧肉", "荤菜", "五花肉、土豆", "10", "18", "80", "60", "套餐",
|
||||
"换套餐名即另一套;无菜谱ID时新建;有菜谱ID/套餐SPU则更新"));
|
||||
list.add(row("2026-08-17", "午餐", "B套餐", "土豆丝", "素菜", null, "4", "18", "80", "60", "套餐", null));
|
||||
list.add(row("2026-08-17", "午餐", "招牌汤", "千张大骨汤", "汤类", null, "0", null, "50", "50", "单品",
|
||||
"单品:类型=单品;套餐名/别名可填别名或「无」;勿填套餐价"));
|
||||
list.add(row("2026-08-17", "早餐", "单品-1", "榨菜肉丝面", "面食", "榨菜,肉丝", "6.4", null, "40", "30", "单品",
|
||||
"不填库存则总库存/移动端可预定默认各600"));
|
||||
list.add(row("2026-08-17", "早餐", "单点-2", "豆浆", "饮品", null, "2", null, "100", "100", "单品", null));
|
||||
list.add(row("2026-08-18", "午餐,晚餐", "无", "番茄蛋汤", "汤类", null, "2", null, "80", "80", "单品",
|
||||
"多餐拆行:无菜品SPU ID时各餐段各自新建;线上未订完可线下继续卖"));
|
||||
return list;
|
||||
}
|
||||
|
||||
/** @param totalStock 总库存;@param mobileQuota 移动端可预定数量 */
|
||||
private static CanteenDailyMenuImportExcel row(String date, String meal, String combo, String dish,
|
||||
String categoryName, String ingredients, String dishPrice, String comboPrice, String onlineStock,
|
||||
String offlineStock, String tip) {
|
||||
String categoryName, String ingredients, String dishPrice, String comboPrice, String totalStock,
|
||||
String mobileQuota, String menuType, String tip) {
|
||||
CanteenDailyMenuImportExcel r = new CanteenDailyMenuImportExcel();
|
||||
r.setBizDate(date);
|
||||
r.setMealName(meal);
|
||||
@@ -130,12 +132,13 @@ public class CanteenDailyMenuImportHelper {
|
||||
if (StrUtil.isNotBlank(comboPrice)) {
|
||||
r.setPriceYuan(new BigDecimal(comboPrice));
|
||||
}
|
||||
if (StrUtil.isNotBlank(onlineStock)) {
|
||||
r.setOnlineStock(Integer.valueOf(onlineStock));
|
||||
if (StrUtil.isNotBlank(totalStock)) {
|
||||
r.setOfflineStock(Integer.valueOf(totalStock));
|
||||
}
|
||||
if (StrUtil.isNotBlank(offlineStock)) {
|
||||
r.setOfflineStock(Integer.valueOf(offlineStock));
|
||||
if (StrUtil.isNotBlank(mobileQuota)) {
|
||||
r.setOnlineStock(Integer.valueOf(mobileQuota));
|
||||
}
|
||||
r.setMenuType(menuType);
|
||||
r.setTip(tip);
|
||||
return r;
|
||||
}
|
||||
@@ -212,8 +215,9 @@ public class CanteenDailyMenuImportHelper {
|
||||
row.setCategoryName(menu.getCategoryName());
|
||||
row.setDishName(StrUtil.blankToDefault(item.getName(), ""));
|
||||
row.setDishPriceYuan(fenToYuan(item.getPrice()));
|
||||
row.setMenuType("套餐");
|
||||
if (written == 0) {
|
||||
row.setTip("套餐块:同套餐名多行=一道排菜;菜谱ID/菜品SPU ID 每行都填(文本),勿改成数字");
|
||||
row.setTip("套餐块:类型=套餐;同套餐名多行=一道排菜;菜谱ID/菜品SPU ID 每行都填(文本)");
|
||||
}
|
||||
rows.add(row);
|
||||
written++;
|
||||
@@ -225,13 +229,15 @@ public class CanteenDailyMenuImportHelper {
|
||||
row.setSpuId(menu.getSpuId() == null ? null : String.valueOf(menu.getSpuId()));
|
||||
row.setBizDate(dateStr);
|
||||
row.setMealName(meal);
|
||||
row.setComboName("无");
|
||||
// 单品:有排菜别名则写入本列,否则「无」
|
||||
row.setComboName(StrUtil.blankToDefault(StrUtil.trim(menu.getDisplayName()), "无"));
|
||||
row.setDishName(menu.getName());
|
||||
row.setCategoryName(menu.getCategoryName());
|
||||
row.setDishPriceYuan(fenToYuan(menu.getPrice()));
|
||||
row.setOnlineStock(menu.getOnlineStock());
|
||||
row.setOfflineStock(menu.getOfflineStock());
|
||||
row.setIngredients(toExcelIngredients(menu.getIngredients()));
|
||||
row.setMenuType("单品");
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
@@ -611,16 +617,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
if (menuId != null) {
|
||||
excelMenuIds.add(menuId);
|
||||
}
|
||||
String groupKey;
|
||||
if (menuId != null) {
|
||||
groupKey = "id:" + menuId + "|" + date + "|" + hourType;
|
||||
}
|
||||
else if (StrUtil.isNotBlank(comboKey) && !isSingleComboMarker(comboKey)) {
|
||||
groupKey = "new:" + date + "|" + hourType + "|" + comboKey;
|
||||
}
|
||||
else {
|
||||
groupKey = "new:" + date + "|" + hourType + "|" + dish;
|
||||
}
|
||||
String groupKey = resolveImportGroupKey(menuId, date, hourType, comboKey, dish, row);
|
||||
groups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(row);
|
||||
}
|
||||
if (CollUtil.isNotEmpty(errors)) {
|
||||
@@ -727,6 +724,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
previewRow.setId(StrUtil.trim(item.getId()));
|
||||
previewRow.setSpuId(StrUtil.trim(item.getSpuId()));
|
||||
previewRow.setChildSpuId(StrUtil.trim(item.getChildSpuId()));
|
||||
previewRow.setMenuType(resolvePreviewMenuType(items, item, cache));
|
||||
previewRow.setGroupKey(groupKey);
|
||||
previewRow.setAction(action);
|
||||
previewRow.setActionLabel(actionLabel);
|
||||
@@ -784,16 +782,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
if (menuId != null) {
|
||||
excelMenuIds.add(menuId);
|
||||
}
|
||||
String groupKey;
|
||||
if (menuId != null) {
|
||||
groupKey = "id:" + menuId + "|" + date + "|" + hourType;
|
||||
}
|
||||
else if (StrUtil.isNotBlank(comboKey) && !isSingleComboMarker(comboKey)) {
|
||||
groupKey = "new:" + date + "|" + hourType + "|" + comboKey;
|
||||
}
|
||||
else {
|
||||
groupKey = "new:" + date + "|" + hourType + "|" + dish;
|
||||
}
|
||||
String groupKey = resolveImportGroupKey(menuId, date, hourType, comboKey, dish, row);
|
||||
groups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(row);
|
||||
}
|
||||
if (CollUtil.isNotEmpty(errors)) {
|
||||
@@ -887,7 +876,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
Integer hourType = groupHour;
|
||||
String comboName = StrUtil.trim(first.getComboName());
|
||||
|
||||
if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) {
|
||||
if (!isComboImportGroup(items, comboName, cache)) {
|
||||
for (CanteenDailyMenuImportExcel item : items) {
|
||||
// 每日线上/线下库存挂菜谱行,不再写入 canteen_spu.stock
|
||||
FindOrCreate r = findOrCreateDish(merchantId, deptId,
|
||||
@@ -898,7 +887,8 @@ public class CanteenDailyMenuImportHelper {
|
||||
createdDish++;
|
||||
}
|
||||
if (schedule(merchantId, deptId, date, hourType, r.spu.getId(), replaceDay, clearedDayHour,
|
||||
cache, pendingMenus, item.getOnlineStock(), item.getOfflineStock())) {
|
||||
cache, pendingMenus, item.getOnlineStock(), item.getOfflineStock(),
|
||||
resolveSingleDisplayName(item))) {
|
||||
scheduled++;
|
||||
}
|
||||
appendDish(st, item.getDishName(), hourType);
|
||||
@@ -955,7 +945,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
createdCombo++;
|
||||
}
|
||||
if (schedule(merchantId, deptId, date, hourType, combo.spu.getId(), replaceDay, clearedDayHour,
|
||||
cache, pendingMenus, onlineStock, offlineStock)) {
|
||||
cache, pendingMenus, onlineStock, offlineStock, sanitizeMenuDisplayName(comboName))) {
|
||||
scheduled++;
|
||||
}
|
||||
}
|
||||
@@ -1510,7 +1500,8 @@ public class CanteenDailyMenuImportHelper {
|
||||
}
|
||||
CanteenDailyMenuImportExcel first = items.get(0);
|
||||
String comboName = StrUtil.trim(first.getComboName());
|
||||
boolean wantsCombo = StrUtil.isNotBlank(comboName) && !isSingleComboMarker(comboName);
|
||||
// 与导入一致:单行别名≠套餐;多行/套餐价/子SPU/套餐SPU 才算套餐
|
||||
boolean wantsCombo = isComboImportGroup(items, comboName, cache);
|
||||
CanteenSpuEntity menuSpu = menu.getSpuId() == null ? null : resolveSpuById(menu.getSpuId(), cache);
|
||||
int menuSpuType = menuSpu == null || menuSpu.getSpuType() == null ? 1 : menuSpu.getSpuType();
|
||||
boolean menuIsCombo = menuSpuType == 2;
|
||||
@@ -1593,7 +1584,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
}
|
||||
String comboName = StrUtil.trim(first.getComboName());
|
||||
try {
|
||||
if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) {
|
||||
if (!isComboImportGroup(items, comboName, cache)) {
|
||||
return upsertSingleById(menu, first, merchantId, deptId, defaultCategoryId, categoryMap, date, hourType,
|
||||
cache);
|
||||
}
|
||||
@@ -1618,6 +1609,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
menu.setStartTime(date);
|
||||
menu.setHourType(hourType);
|
||||
menu.setSpuId(created.spu.getId());
|
||||
menu.setDisplayName(resolveSingleDisplayName(row));
|
||||
menu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(row.getOnlineStock()));
|
||||
menu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(row.getOfflineStock()));
|
||||
canteenDailyMenuService.updateById(menu);
|
||||
@@ -1710,6 +1702,8 @@ public class CanteenDailyMenuImportHelper {
|
||||
targetMenu.setStartTime(date);
|
||||
targetMenu.setHourType(hourType);
|
||||
targetMenu.setSpuId(combo.spu.getId());
|
||||
// 套餐展示名挂排菜;Excel「套餐名」列即短标签
|
||||
targetMenu.setDisplayName(sanitizeMenuDisplayName(comboName));
|
||||
targetMenu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(onlineStock));
|
||||
targetMenu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(offlineStock));
|
||||
canteenDailyMenuService.updateById(targetMenu);
|
||||
@@ -1829,7 +1823,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
*/
|
||||
private boolean schedule(Long merchantId, Long deptId, LocalDate date, Integer hourType, Long spuId,
|
||||
boolean replaceDay, Set<String> clearedDayHour, ImportCache cache,
|
||||
List<CanteenDailyMenuEntity> pendingMenus, Integer onlineStock, Integer offlineStock) {
|
||||
List<CanteenDailyMenuEntity> pendingMenus, Integer onlineStock, Integer offlineStock, String displayName) {
|
||||
String dh = date + "|" + hourType;
|
||||
if (replaceDay && !clearedDayHour.contains(dh)) {
|
||||
canteenDailyMenuService.remove(Wrappers.<CanteenDailyMenuEntity>lambdaQuery()
|
||||
@@ -1849,6 +1843,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
menu.setStartTime(date);
|
||||
menu.setHourType(hourType);
|
||||
menu.setSpuId(spuId);
|
||||
menu.setDisplayName(displayName);
|
||||
menu.setOnlineStock(CanteenDailyMenuStockSupport.quotaOrDefault(onlineStock));
|
||||
menu.setOfflineStock(CanteenDailyMenuStockSupport.quotaOrDefault(offlineStock));
|
||||
pendingMenus.add(menu);
|
||||
@@ -1856,6 +1851,144 @@ public class CanteenDailyMenuImportHelper {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入分组键:显式「单品」按菜名分组;显式「套餐」或非标记套餐名按套餐名分组。
|
||||
*/
|
||||
private static String resolveImportGroupKey(Long menuId, LocalDate date, Integer hourType, String comboKey,
|
||||
String dish, CanteenDailyMenuImportExcel row) {
|
||||
if (menuId != null) {
|
||||
return "id:" + menuId + "|" + date + "|" + hourType;
|
||||
}
|
||||
Boolean explicit = parseMenuType(row == null ? null : row.getMenuType());
|
||||
if (Boolean.FALSE.equals(explicit)) {
|
||||
return "new:" + date + "|" + hourType + "|dish:" + dish;
|
||||
}
|
||||
if (Boolean.TRUE.equals(explicit)
|
||||
|| (StrUtil.isNotBlank(comboKey) && !isSingleComboMarker(comboKey))) {
|
||||
String key = StrUtil.blankToDefault(comboKey, dish);
|
||||
return "new:" + date + "|" + hourType + "|combo:" + key;
|
||||
}
|
||||
return "new:" + date + "|" + hourType + "|dish:" + dish;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否按套餐导入:类型列优先;否则多行同名 / 子SPU / 套餐价 / 菜品SPU为套餐 → 套餐。
|
||||
*/
|
||||
private boolean isComboImportGroup(List<CanteenDailyMenuImportExcel> items, String comboName, ImportCache cache) {
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
return false;
|
||||
}
|
||||
Boolean explicit = resolveGroupMenuType(items);
|
||||
if (Boolean.FALSE.equals(explicit)) {
|
||||
return false;
|
||||
}
|
||||
if (Boolean.TRUE.equals(explicit)) {
|
||||
return true;
|
||||
}
|
||||
if (StrUtil.isBlank(comboName) || isSingleComboMarker(comboName)) {
|
||||
return false;
|
||||
}
|
||||
if (items.size() >= 2) {
|
||||
return true;
|
||||
}
|
||||
CanteenDailyMenuImportExcel first = items.get(0);
|
||||
if (first == null) {
|
||||
return false;
|
||||
}
|
||||
if (StrUtil.isNotBlank(first.getChildSpuId())) {
|
||||
return true;
|
||||
}
|
||||
if (first.getPriceYuan() != null) {
|
||||
return true;
|
||||
}
|
||||
Long spuId = parseExcelLong(first.getSpuId());
|
||||
if (spuId != null) {
|
||||
CanteenSpuEntity spu = resolveSpuById(spuId, cache);
|
||||
if (spu != null && spu.getSpuType() != null && spu.getSpuType() == 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 组内类型列:有明确值时以首个非空为准 */
|
||||
private static Boolean resolveGroupMenuType(List<CanteenDailyMenuImportExcel> items) {
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
return null;
|
||||
}
|
||||
for (CanteenDailyMenuImportExcel item : items) {
|
||||
Boolean t = parseMenuType(item == null ? null : item.getMenuType());
|
||||
if (t != null) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析类型列:true=套餐,false=单品,null=未填/无法识别(走自动规则)。
|
||||
* 旧模板把「填写说明」读到本列时按未填处理。
|
||||
*/
|
||||
private static Boolean parseMenuType(String raw) {
|
||||
String s = StrUtil.trim(raw);
|
||||
if (StrUtil.isBlank(s)) {
|
||||
return null;
|
||||
}
|
||||
if (s.length() > 16 || StrUtil.contains(s, "填写") || StrUtil.contains(s, "导入")
|
||||
|| StrUtil.contains(s, "Excel") || StrUtil.contains(s, "菜谱ID") || StrUtil.contains(s, "类型")) {
|
||||
return null;
|
||||
}
|
||||
String n = s.toLowerCase();
|
||||
if ("套餐".equals(s) || "combo".equals(n) || "2".equals(s) || "是".equals(s) || "y".equals(n)
|
||||
|| "yes".equals(n)) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
if ("单品".equals(s) || "单点".equals(s) || "single".equals(n) || "1".equals(s) || "否".equals(s)
|
||||
|| "n".equals(n) || "no".equals(n)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 预览展示用类型文案 */
|
||||
private String resolvePreviewMenuType(List<CanteenDailyMenuImportExcel> items, CanteenDailyMenuImportExcel item,
|
||||
ImportCache cache) {
|
||||
String raw = item == null ? null : StrUtil.trim(item.getMenuType());
|
||||
Boolean explicit = parseMenuType(raw);
|
||||
if (Boolean.TRUE.equals(explicit)) {
|
||||
return "套餐";
|
||||
}
|
||||
if (Boolean.FALSE.equals(explicit)) {
|
||||
return "单品";
|
||||
}
|
||||
String comboName = StrUtil.trim(item == null ? null : item.getComboName());
|
||||
return isComboImportGroup(items, comboName, cache) ? "套餐" : "单品";
|
||||
}
|
||||
|
||||
/** 单品排菜展示名:取「套餐名/别名」列(「无」等标记视为无别名) */
|
||||
private static String resolveSingleDisplayName(CanteenDailyMenuImportExcel row) {
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
return sanitizeMenuDisplayName(row.getComboName());
|
||||
}
|
||||
|
||||
/** 排菜展示名清洗:过滤说明文案与单品标记 */
|
||||
private static String sanitizeMenuDisplayName(String raw) {
|
||||
String s = StrUtil.trim(raw);
|
||||
if (StrUtil.isBlank(s)) {
|
||||
return null;
|
||||
}
|
||||
if (s.length() > 64 || StrUtil.contains(s, "填写") || StrUtil.contains(s, "导入")
|
||||
|| StrUtil.contains(s, "Excel") || StrUtil.contains(s, "菜谱ID")) {
|
||||
return null;
|
||||
}
|
||||
if (isSingleComboMarker(s)) {
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析/创建单品。
|
||||
* <p>有「菜品SPU ID」/「子SPU ID」→ 按 ID 复用并按需更新名称、配料、价格、分类;无 ID → 一律新建,不再按菜名复用。
|
||||
@@ -2550,6 +2683,7 @@ public class CanteenDailyMenuImportHelper {
|
||||
copy.setId(src.getId());
|
||||
copy.setSpuId(src.getSpuId());
|
||||
copy.setChildSpuId(src.getChildSpuId());
|
||||
copy.setMenuType(src.getMenuType());
|
||||
copy.setTip(src.getTip());
|
||||
return copy;
|
||||
}
|
||||
|
||||
+50
-3
@@ -543,6 +543,54 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
|
||||
}
|
||||
}
|
||||
|
||||
// 套餐订单销量摊到同日同餐段已排菜的组成单品(与实时扣减联动一致)
|
||||
Set<Long> menuSpuIds = menus.stream().map(CanteenDailyMenuEntity::getSpuId).filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
Map<Long, List<com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO>> comboMap =
|
||||
canteenSpuService.loadComboItemVos(new ArrayList<>(menuSpuIds));
|
||||
Set<String> menuKeys = menus.stream()
|
||||
.filter(m -> m.getHourType() != null && m.getSpuId() != null)
|
||||
.map(m -> m.getHourType() + "|" + m.getSpuId())
|
||||
.collect(Collectors.toSet());
|
||||
Map<String, int[]> linkedSold = new HashMap<>();
|
||||
for (Map.Entry<String, int[]> e : soldByHourProduct.entrySet()) {
|
||||
String[] parts = e.getKey().split("\\|", 2);
|
||||
if (parts.length != 2) {
|
||||
continue;
|
||||
}
|
||||
Long comboSpuId;
|
||||
try {
|
||||
comboSpuId = Long.valueOf(parts[1]);
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
continue;
|
||||
}
|
||||
List<com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO> lines = comboMap.get(comboSpuId);
|
||||
if (CollUtil.isEmpty(lines)) {
|
||||
continue;
|
||||
}
|
||||
String hour = parts[0];
|
||||
int[] comboPair = e.getValue();
|
||||
for (com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO line : lines) {
|
||||
if (line == null || line.getChildSpuId() == null) {
|
||||
continue;
|
||||
}
|
||||
String childKey = hour + "|" + line.getChildSpuId();
|
||||
if (!menuKeys.contains(childKey)) {
|
||||
continue;
|
||||
}
|
||||
int qty = line.getQuantity() == null || line.getQuantity() < 1 ? 1 : line.getQuantity();
|
||||
int[] childPair = linkedSold.computeIfAbsent(childKey, ignored -> new int[] { 0, 0 });
|
||||
childPair[0] = Math.addExact(childPair[0], Math.multiplyExact(comboPair[0], qty));
|
||||
childPair[1] = Math.addExact(childPair[1], Math.multiplyExact(comboPair[1], qty));
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, int[]> e : linkedSold.entrySet()) {
|
||||
int[] pair = soldByHourProduct.computeIfAbsent(e.getKey(), ignored -> new int[] { 0, 0 });
|
||||
pair[0] = Math.addExact(pair[0], e.getValue()[0]);
|
||||
pair[1] = Math.addExact(pair[1], e.getValue()[1]);
|
||||
}
|
||||
|
||||
List<CanteenDailyMenuEntity> toUpdate = new ArrayList<>();
|
||||
for (CanteenDailyMenuEntity menu : menus) {
|
||||
if (menu.getHourType() == null || menu.getSpuId() == null) {
|
||||
@@ -641,8 +689,8 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
|
||||
for (DailyMenuAppVO vo : list) {
|
||||
int type = vo.getSpuType() == null ? 1 : vo.getSpuType();
|
||||
vo.setSpuType(type);
|
||||
// 旧数据仅写了 introduction:App/收银展示套餐名时回落
|
||||
if (type == 2 && StrUtil.isBlank(vo.getComboName()) && StrUtil.isNotBlank(vo.getIntroduction())) {
|
||||
// 旧数据仅写了 introduction:展示名回落(SQL 已优先排菜 display_name)
|
||||
if (StrUtil.isBlank(vo.getComboName()) && StrUtil.isNotBlank(vo.getIntroduction())) {
|
||||
vo.setComboName(vo.getIntroduction().trim());
|
||||
}
|
||||
if (type == 2) {
|
||||
@@ -650,7 +698,6 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
|
||||
}
|
||||
else {
|
||||
vo.setComboItems(Collections.emptyList());
|
||||
vo.setComboName(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+213
-35
@@ -6,6 +6,7 @@ import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
|
||||
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
|
||||
import com.bsd.pjcloud.canteen.vo.DailyMenuAdminVO;
|
||||
import com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO;
|
||||
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -17,11 +18,22 @@ import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 每日菜谱分池库存展示。
|
||||
* 每日菜谱库存展示与剩余计算。
|
||||
* <p>
|
||||
* {@code online_stock}/{@code offline_stock} 为人工配额(下单不改);
|
||||
* {@code online_sold}/{@code offline_sold} 为净销量;剩余 = 配额 − 销量。
|
||||
* App 的 {@code stock}=线上剩余,安卓 AD 的 {@code stock}=线下剩余。
|
||||
* 库字段约定(不改表结构):
|
||||
* <ul>
|
||||
* <li>{@code offline_stock} = <b>总库存</b>(人工)</li>
|
||||
* <li>{@code online_stock} = <b>移动端可预订数量</b>上限(人工)</li>
|
||||
* <li>{@code online_sold}/{@code offline_sold} = 移动端已预订 / 消费机已销售净销量</li>
|
||||
* </ul>
|
||||
* 共享总库存:线上未订完可继续线下卖。
|
||||
* <pre>
|
||||
* 剩余总库存 = max(0, 总库存 − 移动端已预订 − 消费机已销售)
|
||||
* 移动端可预订剩余 = min(移动端上限 − 已预订, 剩余总库存)
|
||||
* AD stock = max(0, 总库存 − 移动端已预订)
|
||||
* </pre>
|
||||
* PC 管理端只展示总库存、移动端上限、移动端已订、消费机已售、剩余总库存。
|
||||
* 扣减仍受「剩余总库存」边界约束,防止超卖。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@@ -37,20 +49,73 @@ public class CanteenDailyMenuStockSupport {
|
||||
return sold == null || sold < 0 ? 0 : sold;
|
||||
}
|
||||
|
||||
/** 剩余 = max(0, 配额 − 销量) */
|
||||
/** @deprecated 独立分池已废弃,请用 {@link #sharedRemain} / {@link #onlineRemain} */
|
||||
public static int remainOf(Integer quota, Integer sold) {
|
||||
return Math.max(0, quotaOrDefault(quota) - soldOrDefault(sold));
|
||||
}
|
||||
|
||||
/** 总库存(库字段 offline_stock) */
|
||||
public static int totalStockOf(Integer offlineStockAsTotal) {
|
||||
return quotaOrDefault(offlineStockAsTotal);
|
||||
}
|
||||
|
||||
public static int totalStockOf(CanteenDailyMenuEntity menu) {
|
||||
return menu == null ? 0 : totalStockOf(menu.getOfflineStock());
|
||||
}
|
||||
|
||||
/** 移动端可预订上限(库字段 online_stock) */
|
||||
public static int mobileQuotaOf(CanteenDailyMenuEntity menu) {
|
||||
return menu == null ? 0 : quotaOrDefault(menu.getOnlineStock());
|
||||
}
|
||||
|
||||
/** 剩余总库存 = 总库存 − 线上已订 − 线下已售 */
|
||||
public static int sharedRemain(Integer totalStock, Integer onlineSold, Integer offlineSold) {
|
||||
return Math.max(0, totalStockOf(totalStock) - soldOrDefault(onlineSold) - soldOrDefault(offlineSold));
|
||||
}
|
||||
|
||||
public static int sharedRemain(CanteenDailyMenuEntity menu) {
|
||||
if (menu == null) {
|
||||
return 0;
|
||||
}
|
||||
return sharedRemain(menu.getOfflineStock(), menu.getOnlineSold(), menu.getOfflineSold());
|
||||
}
|
||||
|
||||
/** 移动端可预订剩余 */
|
||||
public static int onlineRemain(Integer totalStock, Integer mobileQuota, Integer onlineSold, Integer offlineSold) {
|
||||
int shared = sharedRemain(totalStock, onlineSold, offlineSold);
|
||||
int mobileLeft = Math.max(0, quotaOrDefault(mobileQuota) - soldOrDefault(onlineSold));
|
||||
return Math.min(mobileLeft, shared);
|
||||
}
|
||||
|
||||
public static int onlineRemain(CanteenDailyMenuEntity menu) {
|
||||
if (menu == null) {
|
||||
return 0;
|
||||
}
|
||||
return onlineRemain(menu.getOfflineStock(), menu.getOnlineStock(), menu.getOnlineSold(), menu.getOfflineSold());
|
||||
}
|
||||
|
||||
/**
|
||||
* AD 返回的 stock:总库存 − 移动端已预订(不含消费机已售;已售由 offline_sold 单独体现)。
|
||||
*/
|
||||
public static int adStockOf(Integer totalStock, Integer onlineSold) {
|
||||
return Math.max(0, totalStockOf(totalStock) - soldOrDefault(onlineSold));
|
||||
}
|
||||
|
||||
public static int adStockOf(CanteenDailyMenuEntity menu) {
|
||||
if (menu == null) {
|
||||
return 0;
|
||||
}
|
||||
return adStockOf(menu.getOfflineStock(), menu.getOnlineSold());
|
||||
}
|
||||
|
||||
public static int channelRemain(CanteenDailyMenuEntity menu, boolean offline) {
|
||||
if (menu == null) {
|
||||
return 0;
|
||||
}
|
||||
return offline ? remainOf(menu.getOfflineStock(), menu.getOfflineSold())
|
||||
: remainOf(menu.getOnlineStock(), menu.getOnlineSold());
|
||||
return offline ? adStockOf(menu) : onlineRemain(menu);
|
||||
}
|
||||
|
||||
/** App:stock = 线上剩余;onlineStock/offlineStock 仍透出配额 */
|
||||
/** App:stock = 线上剩余 */
|
||||
public void fillAppOnlineRemain(List<DailyMenuAppVO> rows, Long merchantId, LocalDate mealDate) {
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
return;
|
||||
@@ -58,9 +123,10 @@ public class CanteenDailyMenuStockSupport {
|
||||
for (DailyMenuAppVO row : rows) {
|
||||
applyAppQuotasAndRemains(row, true);
|
||||
}
|
||||
applyAppComboLinkedRemain(rows, true);
|
||||
}
|
||||
|
||||
/** 安卓消费机:stock = 线下剩余(字段名不变) */
|
||||
/** 安卓消费机:stock = 总库存 − 移动端已预订 */
|
||||
public void fillAdOfflineRemain(List<DailyMenuAppVO> rows, Long merchantId, LocalDate mealDate) {
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
return;
|
||||
@@ -68,6 +134,7 @@ public class CanteenDailyMenuStockSupport {
|
||||
for (DailyMenuAppVO row : rows) {
|
||||
applyAppQuotasAndRemains(row, false);
|
||||
}
|
||||
applyAppComboLinkedRemain(rows, false);
|
||||
}
|
||||
|
||||
public void fillAdOfflineRemainByRowDate(List<DailyMenuAppVO> rows, Long merchantId) {
|
||||
@@ -87,45 +154,158 @@ public class CanteenDailyMenuStockSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/** PC:配额与剩余分列展示 */
|
||||
/** PC:总库存 / 移动端上限 / 已订与已售 / 剩余总库存 */
|
||||
public void fillAdminRemain(List<DailyMenuAdminVO> rows, Long merchantId, LocalDate mealDate) {
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
return;
|
||||
}
|
||||
Map<String, DailyMenuAdminVO> byMealSpu = new HashMap<>();
|
||||
for (DailyMenuAdminVO row : rows) {
|
||||
int onlineQuota = quotaOrDefault(row.getOnlineStock());
|
||||
int offlineQuota = quotaOrDefault(row.getOfflineStock());
|
||||
int onlineRemain = remainOf(onlineQuota, row.getOnlineSold());
|
||||
int offlineRemain = remainOf(offlineQuota, row.getOfflineSold());
|
||||
row.setOnlineStock(onlineQuota);
|
||||
row.setOfflineStock(offlineQuota);
|
||||
// 入库:offlineStock=总库存,onlineStock=移动端上限
|
||||
int total = totalStockOf(row.getOfflineStock());
|
||||
int mobile = quotaOrDefault(row.getOnlineStock());
|
||||
int onlineSold = soldOrDefault(row.getOnlineSold());
|
||||
int offlineSold = soldOrDefault(row.getOfflineSold());
|
||||
int shared = sharedRemain(total, onlineSold, offlineSold);
|
||||
int onlineRemain = onlineRemain(total, mobile, onlineSold, offlineSold);
|
||||
row.setTotalStock(total);
|
||||
row.setOnlineStock(mobile);
|
||||
// offlineStock 保持总库存原值,便于编辑回传;列表展示用 totalStock / offlineSold
|
||||
row.setOfflineStock(total);
|
||||
row.setOfflineSold(offlineSold);
|
||||
row.setOnlineSold(onlineSold);
|
||||
row.setRemainOnlineStock(onlineRemain);
|
||||
row.setRemainOfflineStock(offlineRemain);
|
||||
row.setStock(offlineRemain);
|
||||
row.setRemainStock(onlineRemain + offlineRemain);
|
||||
row.setRemainOfflineStock(shared);
|
||||
row.setStock(shared);
|
||||
row.setRemainStock(shared);
|
||||
if (row.getSpuId() != null) {
|
||||
byMealSpu.put(mealSpuKey(row.getHourType(), row.getSpuId()), row);
|
||||
}
|
||||
}
|
||||
for (DailyMenuAdminVO row : rows) {
|
||||
if (!Objects.equals(row.getSpuType(), 2) || CollUtil.isEmpty(row.getComboItems())) {
|
||||
continue;
|
||||
}
|
||||
int formableOnline = formableFromChildren(row.getComboItems(), byMealSpu, row.getHourType(), true);
|
||||
int formableOffline = formableFromChildren(row.getComboItems(), byMealSpu, row.getHourType(), false);
|
||||
if (formableOnline >= 0) {
|
||||
row.setRemainOnlineStock(Math.min(row.getRemainOnlineStock(), formableOnline));
|
||||
}
|
||||
if (formableOffline >= 0) {
|
||||
row.setRemainOfflineStock(Math.min(row.getRemainOfflineStock(), formableOffline));
|
||||
}
|
||||
row.setStock(row.getRemainOfflineStock());
|
||||
row.setRemainStock(row.getRemainOfflineStock());
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyAppQuotasAndRemains(DailyMenuAppVO row, boolean stockAsOnline) {
|
||||
int onlineQuota = quotaOrDefault(row.getOnlineStock());
|
||||
int offlineQuota = quotaOrDefault(row.getOfflineStock());
|
||||
int onlineRemain = remainOf(onlineQuota, row.getOnlineSold());
|
||||
int offlineRemain = remainOf(offlineQuota, row.getOfflineSold());
|
||||
row.setOnlineStock(onlineQuota);
|
||||
row.setOfflineStock(offlineQuota);
|
||||
int total = totalStockOf(row.getOfflineStock());
|
||||
int mobile = quotaOrDefault(row.getOnlineStock());
|
||||
int onlineSold = soldOrDefault(row.getOnlineSold());
|
||||
int offlineSold = soldOrDefault(row.getOfflineSold());
|
||||
int shared = sharedRemain(total, onlineSold, offlineSold);
|
||||
int onlineRemain = onlineRemain(total, mobile, onlineSold, offlineSold);
|
||||
int adStock = adStockOf(total, onlineSold);
|
||||
row.setOnlineStock(mobile);
|
||||
row.setOfflineStock(total);
|
||||
row.setOnlineSold(onlineSold);
|
||||
row.setOfflineSold(offlineSold);
|
||||
row.setRemainOnlineStock(onlineRemain);
|
||||
row.setRemainOfflineStock(offlineRemain);
|
||||
row.setStock(stockAsOnline ? onlineRemain : offlineRemain);
|
||||
// AD:remainOfflineStock / stock = 总库存 − 线上已订;App 侧 remainOffline 仍给剩余总库存供参考
|
||||
row.setRemainOfflineStock(stockAsOnline ? shared : adStock);
|
||||
row.setStock(stockAsOnline ? onlineRemain : adStock);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安卓 SPU 查看接口:若当日菜谱存在对应行,则 {@code stock} 覆盖为线下剩余;无菜谱行则保留 canteen_spu.stock。
|
||||
*
|
||||
* @param rows 已组装的 AD VO(含 merchantId、categoryType)
|
||||
* @param menus 当日(或指定日)相关每日菜谱行
|
||||
* @param hourTypeOverride 请求显式餐段;为空时从 VO.categoryType 解析
|
||||
* @param offline true=线下剩余(AD),false=线上剩余(App 详情复用)
|
||||
* 套餐有效剩余 = min(套餐行剩余, 各组成菜在当日同餐段可组成份数)。
|
||||
*/
|
||||
private static void applyAppComboLinkedRemain(List<DailyMenuAppVO> rows, boolean stockAsOnline) {
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
return;
|
||||
}
|
||||
Map<String, DailyMenuAppVO> byMealSpu = new HashMap<>();
|
||||
for (DailyMenuAppVO row : rows) {
|
||||
if (row != null && row.getSpuId() != null) {
|
||||
byMealSpu.put(mealSpuKey(row.getHourType(), row.getSpuId()), row);
|
||||
}
|
||||
}
|
||||
for (DailyMenuAppVO row : rows) {
|
||||
if (row == null || !Objects.equals(row.getSpuType(), 2) || CollUtil.isEmpty(row.getComboItems())) {
|
||||
continue;
|
||||
}
|
||||
int formable = formableFromAppChildren(row.getComboItems(), byMealSpu, row.getHourType(), stockAsOnline);
|
||||
if (formable < 0) {
|
||||
continue;
|
||||
}
|
||||
int own = stockAsOnline ? nullToZero(row.getRemainOnlineStock()) : nullToZero(row.getRemainOfflineStock());
|
||||
int effective = Math.min(own, formable);
|
||||
if (stockAsOnline) {
|
||||
row.setRemainOnlineStock(effective);
|
||||
}
|
||||
else {
|
||||
row.setRemainOfflineStock(effective);
|
||||
}
|
||||
row.setStock(effective);
|
||||
}
|
||||
}
|
||||
|
||||
private static int formableFromAppChildren(List<OnlineSpuComboItemVO> items, Map<String, DailyMenuAppVO> byMealSpu,
|
||||
Integer hourType, boolean online) {
|
||||
if (CollUtil.isEmpty(items) || byMealSpu == null || byMealSpu.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
int formable = Integer.MAX_VALUE;
|
||||
boolean any = false;
|
||||
for (OnlineSpuComboItemVO item : items) {
|
||||
if (item == null || item.getChildSpuId() == null) {
|
||||
continue;
|
||||
}
|
||||
DailyMenuAppVO child = byMealSpu.get(mealSpuKey(hourType, item.getChildSpuId()));
|
||||
if (child == null) {
|
||||
continue;
|
||||
}
|
||||
any = true;
|
||||
int qty = item.getQuantity() == null || item.getQuantity() < 1 ? 1 : item.getQuantity();
|
||||
int childRemain = online ? nullToZero(child.getRemainOnlineStock())
|
||||
: nullToZero(child.getRemainOfflineStock());
|
||||
formable = Math.min(formable, childRemain / qty);
|
||||
}
|
||||
return any ? formable : -1;
|
||||
}
|
||||
|
||||
private static int formableFromChildren(List<OnlineSpuComboItemVO> items, Map<String, DailyMenuAdminVO> byMealSpu,
|
||||
Integer hourType, boolean online) {
|
||||
if (CollUtil.isEmpty(items) || byMealSpu == null || byMealSpu.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
int formable = Integer.MAX_VALUE;
|
||||
boolean any = false;
|
||||
for (OnlineSpuComboItemVO item : items) {
|
||||
if (item == null || item.getChildSpuId() == null) {
|
||||
continue;
|
||||
}
|
||||
DailyMenuAdminVO child = byMealSpu.get(mealSpuKey(hourType, item.getChildSpuId()));
|
||||
if (child == null) {
|
||||
continue;
|
||||
}
|
||||
any = true;
|
||||
int qty = item.getQuantity() == null || item.getQuantity() < 1 ? 1 : item.getQuantity();
|
||||
int childRemain = online ? nullToZero(child.getRemainOnlineStock())
|
||||
: nullToZero(child.getRemainOfflineStock());
|
||||
formable = Math.min(formable, childRemain / qty);
|
||||
}
|
||||
return any ? formable : -1;
|
||||
}
|
||||
|
||||
private static String mealSpuKey(Integer hourType, Long spuId) {
|
||||
return (hourType == null ? "_" : hourType) + "|" + spuId;
|
||||
}
|
||||
|
||||
private static int nullToZero(Integer v) {
|
||||
return v == null ? 0 : v;
|
||||
}
|
||||
|
||||
public void overlaySpuViewStock(List<ADCanteenSpuListVO> rows, List<CanteenDailyMenuEntity> menus,
|
||||
Integer hourTypeOverride, boolean offline) {
|
||||
if (CollUtil.isEmpty(rows) || CollUtil.isEmpty(menus)) {
|
||||
@@ -144,7 +324,6 @@ public class CanteenDailyMenuStockSupport {
|
||||
}
|
||||
Integer hourType = hourTypeOverride != null ? hourTypeOverride : parseHourType(row.getCategoryType());
|
||||
if (hourType == null) {
|
||||
// 分类未标餐段时:同一商户+SPU 当日若只有一行菜谱,仍可覆盖
|
||||
CanteenDailyMenuEntity only = findUniqueMenu(menus, row.getMerchantId(), row.getId());
|
||||
if (only != null) {
|
||||
row.setStock(channelRemain(only, offline));
|
||||
@@ -178,7 +357,6 @@ public class CanteenDailyMenuStockSupport {
|
||||
return merchantId + "|" + hourType + "|" + spuId;
|
||||
}
|
||||
|
||||
/** categoryType 支持 "2" 或 "2,3";取第一个有效餐段。 */
|
||||
public static Integer parseHourType(String categoryType) {
|
||||
if (StrUtil.isBlank(categoryType)) {
|
||||
return null;
|
||||
|
||||
+170
-18
@@ -84,6 +84,8 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
if (updateStockReqDTO == null || CollUtil.isEmpty(updateStockReqDTO.getItems())) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_ITEMS_EMPTY));
|
||||
}
|
||||
// AD 旧机可不传 stockChannel:默认 OFFLINE,走每日菜谱线下池(不再扣 canteen_spu.stock)
|
||||
normalizeDailyStockChannel(updateStockReqDTO);
|
||||
if (isDailyChannel(updateStockReqDTO.getStockChannel())) {
|
||||
return updateDailyMenuChannelStock(updateStockReqDTO);
|
||||
}
|
||||
@@ -133,6 +135,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
|| item.getIncrCount() == null || item.getIncrCount() == 0 || item.getBeforeStock() == null)) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_BIZ_PARAM_BAD));
|
||||
}
|
||||
normalizeDailyStockChannel(dto);
|
||||
if (isDailyChannel(dto.getStockChannel())) {
|
||||
return updateDailyMenuChannelStockIdempotent(dto, applyOperation, rollbackOperation);
|
||||
}
|
||||
@@ -218,13 +221,50 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
return "ONLINE".equals(ch) || "OFFLINE".equals(ch);
|
||||
}
|
||||
|
||||
/** 非幂等分池扣减:改写 daily_menu 销量字段(安卓仍调 /ad/updateSkuStock,由硬件补 stockChannel)。 */
|
||||
/** 空通道默认 OFFLINE,兼容未升级的安卓消费机。 */
|
||||
private static void normalizeDailyStockChannel(CanteenSpuUpdateStockReqDTO dto) {
|
||||
if (dto != null && StrUtil.isBlank(dto.getStockChannel())) {
|
||||
dto.setStockChannel("OFFLINE");
|
||||
}
|
||||
}
|
||||
|
||||
/** 分池扣减缺 merchantId 时,从首个 SPU 回填(AD 旧请求常不传)。 */
|
||||
private Long resolveDailyStockMerchantId(CanteenSpuUpdateStockReqDTO dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
if (dto.getMerchantId() != null) {
|
||||
return dto.getMerchantId();
|
||||
}
|
||||
if (CollUtil.isEmpty(dto.getItems())) {
|
||||
return null;
|
||||
}
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : dto.getItems()) {
|
||||
if (item == null || item.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
CanteenSpuEntity spu = baseMapper.selectById(item.getId());
|
||||
if (spu != null && spu.getMerchantId() != null) {
|
||||
dto.setMerchantId(spu.getMerchantId());
|
||||
return spu.getMerchantId();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 非幂等分池扣减:改写 daily_menu 销量字段(AD 空 stockChannel 已默认 OFFLINE)。 */
|
||||
private R updateDailyMenuChannelStock(CanteenSpuUpdateStockReqDTO dto) {
|
||||
if (dto.getMerchantId() == null) {
|
||||
if (resolveDailyStockMerchantId(dto) == null) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_BIZ_PARAM_BAD));
|
||||
}
|
||||
boolean offline = "OFFLINE".equals(StrUtil.trim(dto.getStockChannel()));
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : dto.getItems()) {
|
||||
boolean offline = !"ONLINE".equals(StrUtil.trim(dto.getStockChannel()));
|
||||
List<CanteenSpuUpdateStockReqDTO.Item> items = expandDailyMenuLinkedStockItems(dto.getMerchantId(), offline,
|
||||
dto.getItems(), false);
|
||||
if (CollUtil.isEmpty(items)) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_ITEMS_EMPTY));
|
||||
}
|
||||
Map<String, Integer> remainCursor = new HashMap<>();
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : items) {
|
||||
if (item == null || item.getId() == null || item.getIncrCount() == null
|
||||
|| item.getIncrCount() == 0 || item.getIncrCount() == Integer.MIN_VALUE
|
||||
|| item.getMealDate() == null || item.getHourType() == null) {
|
||||
@@ -233,17 +273,19 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
CanteenDailyMenuEntity menu = findDailyMenuRow(dto.getMerchantId(), item.getMealDate(), item.getHourType(),
|
||||
item.getId());
|
||||
if (menu == null) {
|
||||
// 分池扣减按「商户+日期+餐段+SPU」查每日菜谱;查不到不是 canteen_spu 不存在
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_DAILY_MENU_PRODUCT_BAD));
|
||||
}
|
||||
int current = CanteenDailyMenuStockSupport.channelRemain(menu, offline);
|
||||
String key = dailyStockItemKey(item);
|
||||
int current = remainCursor.computeIfAbsent(key,
|
||||
k -> CanteenDailyMenuStockSupport.channelRemain(menu, offline));
|
||||
if (item.getIncrCount() < 0 && Math.abs(item.getIncrCount()) > current) {
|
||||
CanteenSpuEntity spu = baseMapper.selectById(item.getId());
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_PRODUCT_STOCK_LOW,
|
||||
StrUtil.blankToDefault(spu == null ? null : spu.getName(), "商品")));
|
||||
}
|
||||
remainCursor.put(key, Math.addExact(current, item.getIncrCount()));
|
||||
}
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : dto.getItems()) {
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : items) {
|
||||
int updated = offline
|
||||
? canteenDailyMenuMapper.updateOfflineStockDelta(dto.getMerchantId(), item.getMealDate(),
|
||||
item.getHourType(), item.getId(), item.getIncrCount(), null)
|
||||
@@ -259,11 +301,11 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
/** 幂等分池扣减:改写 daily_menu.online_sold / offline_sold(配额字段不动)。 */
|
||||
private R updateDailyMenuChannelStockIdempotent(CanteenSpuUpdateStockReqDTO dto, boolean applyOperation,
|
||||
boolean rollbackOperation) {
|
||||
if (dto.getMerchantId() == null
|
||||
if (resolveDailyStockMerchantId(dto) == null
|
||||
|| dto.getItems().stream().anyMatch(item -> item.getMealDate() == null || item.getHourType() == null)) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_BIZ_PARAM_BAD));
|
||||
}
|
||||
boolean offline = "OFFLINE".equals(StrUtil.trim(dto.getStockChannel()));
|
||||
boolean offline = !"ONLINE".equals(StrUtil.trim(dto.getStockChannel()));
|
||||
String stateKey = ORDER_STOCK_BIZ_KEY_PREFIX + "daily:" + dto.getStockChannel() + ":" + dto.getBizId();
|
||||
String state = stringRedisTemplate.opsForValue().get(stateKey);
|
||||
if (applyOperation && ("APPLIED".equals(state) || "ROLLED_BACK".equals(state))) {
|
||||
@@ -272,13 +314,19 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
if (rollbackOperation && ("ROLLED_BACK".equals(state) || ORDER_STOCK_BIZ_STATE_COMPENSATED.equals(state))) {
|
||||
return R.ok();
|
||||
}
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : dto.getItems()) {
|
||||
// 套餐行展开为组成单品同日同餐段销量联动;ROLLBACK 对同一 items 再展开一次,保证对称回补
|
||||
List<CanteenSpuUpdateStockReqDTO.Item> items = expandDailyMenuLinkedStockItems(dto.getMerchantId(), offline,
|
||||
dto.getItems(), true);
|
||||
Map<String, Integer> remainCursor = new HashMap<>();
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : items) {
|
||||
CanteenDailyMenuEntity menu = findDailyMenuRow(dto.getMerchantId(), item.getMealDate(), item.getHourType(),
|
||||
item.getId());
|
||||
if (menu == null) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_DAILY_MENU_PRODUCT_BAD));
|
||||
}
|
||||
int current = CanteenDailyMenuStockSupport.channelRemain(menu, offline);
|
||||
String key = dailyStockItemKey(item);
|
||||
int current = remainCursor.computeIfAbsent(key,
|
||||
k -> CanteenDailyMenuStockSupport.channelRemain(menu, offline));
|
||||
int expectedStock = item.getBeforeStock();
|
||||
if (expectedStock < 0) {
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_SNAPSHOT_BAD));
|
||||
@@ -288,8 +336,6 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_PRODUCT_STOCK_LOW,
|
||||
StrUtil.blankToDefault(spu == null ? null : spu.getName(), "商品")));
|
||||
}
|
||||
// 正增量恢复是可交换操作,跳过剩余量快照校验,避免退款恢复被并发下单等无关变更误拒;
|
||||
// 负增量扣减保留快照校验,防止不同业务号基于同一剩余量重复判断导致超卖。
|
||||
if (applyOperation && item.getIncrCount() < 0 && current != expectedStock) {
|
||||
CanteenSpuEntity spu = baseMapper.selectById(item.getId());
|
||||
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_CHANGED,
|
||||
@@ -303,8 +349,11 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
StrUtil.blankToDefault(spu == null ? null : spu.getName(), "商品")));
|
||||
}
|
||||
}
|
||||
int simulateDelta = applyOperation ? item.getIncrCount()
|
||||
: (rollbackOperation && "APPLIED".equals(state) ? -item.getIncrCount() : 0);
|
||||
remainCursor.put(key, Math.addExact(current, simulateDelta));
|
||||
}
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : dto.getItems()) {
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : items) {
|
||||
int delta = applyOperation ? item.getIncrCount() : -item.getIncrCount();
|
||||
int updated;
|
||||
if (rollbackOperation && "APPLIED".equals(state)) {
|
||||
@@ -319,10 +368,6 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
continue;
|
||||
}
|
||||
if (applyOperation) {
|
||||
// 正增量恢复直接原子累加:expected 传 null 跳过剩余量匹配条件,SQL 也不带任何边界条件,
|
||||
// 销量回退由 GREATEST 钳制到 0 为止——菜谱重导、按订单重算销量、配额为 0 等统计口径
|
||||
// 都不得阻断退款/补偿;幂等由 bizId 锁和 Redis 状态保证。
|
||||
// 负增量扣减保留快照匹配条件更新,防止不同业务号基于同一剩余量重复判断导致超卖。
|
||||
Integer expected = delta > 0 ? null : item.getBeforeStock();
|
||||
updated = offline
|
||||
? canteenDailyMenuMapper.updateOfflineStockDelta(dto.getMerchantId(), item.getMealDate(),
|
||||
@@ -341,6 +386,113 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐销量联动:同日同餐段若组成单品也在菜谱中,套餐扣/补时按组成份数同步改写单品销量。
|
||||
* 单品售出会通过展示/校验侧的「可组成套餐数」压低套餐可售,此处不反向改写套餐 sold,避免与订单销量口径打架。
|
||||
*/
|
||||
private List<CanteenSpuUpdateStockReqDTO.Item> expandDailyMenuLinkedStockItems(Long merchantId, boolean offline,
|
||||
List<CanteenSpuUpdateStockReqDTO.Item> source, boolean fillMissingBeforeStock) {
|
||||
if (CollUtil.isEmpty(source)) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, Integer> callerBefore = new HashMap<>();
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : source) {
|
||||
if (item != null && item.getId() != null && item.getMealDate() != null && item.getHourType() != null
|
||||
&& item.getBeforeStock() != null) {
|
||||
callerBefore.put(dailyStockItemKey(item), item.getBeforeStock());
|
||||
}
|
||||
}
|
||||
Set<Long> spuIds = source.stream().filter(Objects::nonNull).map(CanteenSpuUpdateStockReqDTO.Item::getId)
|
||||
.filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<Long, CanteenSpuEntity> spuMap = spuIds.isEmpty() ? Map.of()
|
||||
: listByIds(spuIds).stream().collect(Collectors.toMap(CanteenSpuEntity::getId, s -> s, (a, b) -> a));
|
||||
List<Long> comboIds = spuMap.values().stream()
|
||||
.filter(s -> s != null && Objects.equals(s.getSpuType(), 2) && s.getId() != null)
|
||||
.map(CanteenSpuEntity::getId).distinct().collect(Collectors.toList());
|
||||
Map<Long, List<CanteenSpuComboItemEntity>> comboMap = loadComboItems(comboIds);
|
||||
|
||||
List<CanteenSpuUpdateStockReqDTO.Item> expanded = new ArrayList<>();
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : source) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
expanded.add(copyStockItem(item));
|
||||
CanteenSpuEntity spu = spuMap.get(item.getId());
|
||||
if (spu == null || !Objects.equals(spu.getSpuType(), 2)) {
|
||||
continue;
|
||||
}
|
||||
List<CanteenSpuComboItemEntity> lines = comboMap.get(item.getId());
|
||||
if (CollUtil.isEmpty(lines)) {
|
||||
continue;
|
||||
}
|
||||
for (CanteenSpuComboItemEntity line : lines) {
|
||||
if (line == null || line.getChildSpuId() == null) {
|
||||
continue;
|
||||
}
|
||||
// 组成菜未排进当日同餐段则不联动
|
||||
if (findDailyMenuRow(merchantId, item.getMealDate(), item.getHourType(),
|
||||
line.getChildSpuId()) == null) {
|
||||
continue;
|
||||
}
|
||||
int qty = line.getQuantity() == null || line.getQuantity() < 1 ? 1 : line.getQuantity();
|
||||
CanteenSpuUpdateStockReqDTO.Item child = new CanteenSpuUpdateStockReqDTO.Item();
|
||||
child.setId(line.getChildSpuId());
|
||||
child.setIncrCount(Math.multiplyExact(item.getIncrCount(), qty));
|
||||
child.setMealDate(item.getMealDate());
|
||||
child.setHourType(item.getHourType());
|
||||
expanded.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, CanteenSpuUpdateStockReqDTO.Item> merged = new LinkedHashMap<>();
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : expanded) {
|
||||
if (item.getId() == null || item.getMealDate() == null || item.getHourType() == null
|
||||
|| item.getIncrCount() == null || item.getIncrCount() == 0) {
|
||||
continue;
|
||||
}
|
||||
String key = dailyStockItemKey(item);
|
||||
CanteenSpuUpdateStockReqDTO.Item exist = merged.get(key);
|
||||
if (exist == null) {
|
||||
merged.put(key, copyStockItem(item));
|
||||
}
|
||||
else {
|
||||
exist.setIncrCount(Math.addExact(exist.getIncrCount(), item.getIncrCount()));
|
||||
}
|
||||
}
|
||||
|
||||
List<CanteenSpuUpdateStockReqDTO.Item> result = new ArrayList<>(merged.size());
|
||||
for (CanteenSpuUpdateStockReqDTO.Item item : merged.values()) {
|
||||
if (item.getIncrCount() == null || item.getIncrCount() == 0) {
|
||||
continue;
|
||||
}
|
||||
Integer before = callerBefore.get(dailyStockItemKey(item));
|
||||
if (before == null && fillMissingBeforeStock) {
|
||||
CanteenDailyMenuEntity menu = findDailyMenuRow(merchantId, item.getMealDate(), item.getHourType(),
|
||||
item.getId());
|
||||
before = menu == null ? 0 : CanteenDailyMenuStockSupport.channelRemain(menu, offline);
|
||||
}
|
||||
if (before != null) {
|
||||
item.setBeforeStock(before);
|
||||
}
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CanteenSpuUpdateStockReqDTO.Item copyStockItem(CanteenSpuUpdateStockReqDTO.Item src) {
|
||||
CanteenSpuUpdateStockReqDTO.Item copy = new CanteenSpuUpdateStockReqDTO.Item();
|
||||
copy.setId(src.getId());
|
||||
copy.setIncrCount(src.getIncrCount());
|
||||
copy.setBeforeStock(src.getBeforeStock());
|
||||
copy.setMealDate(src.getMealDate());
|
||||
copy.setHourType(src.getHourType());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static String dailyStockItemKey(CanteenSpuUpdateStockReqDTO.Item item) {
|
||||
return item.getMealDate() + "|" + item.getHourType() + "|" + item.getId();
|
||||
}
|
||||
|
||||
private CanteenDailyMenuEntity findDailyMenuRow(Long merchantId, java.time.LocalDate mealDate, Integer hourType,
|
||||
Long spuId) {
|
||||
return canteenDailyMenuMapper.selectOne(Wrappers.<CanteenDailyMenuEntity>lambdaQuery()
|
||||
|
||||
+22
-10
@@ -6,6 +6,7 @@
|
||||
<id property="id" column="id"/>
|
||||
<result property="startTime" column="start_time"/>
|
||||
<result property="spuId" column="spu_id"/>
|
||||
<result property="displayName" column="display_name"/>
|
||||
<result property="hourType" column="hour_type"/>
|
||||
<result property="onlineStock" column="online_stock"/>
|
||||
<result property="offlineStock" column="offline_stock"/>
|
||||
@@ -62,7 +63,7 @@
|
||||
sp.dept_id,
|
||||
t.create_by,
|
||||
IFNULL(sp.spu_type, 1) as spu_type,
|
||||
sp.combo_name,
|
||||
COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name,
|
||||
sp.introduction
|
||||
FROM canteen_daily_menu t
|
||||
LEFT JOIN canteen_spu sp
|
||||
@@ -86,7 +87,7 @@
|
||||
sp.id AS spu_id,
|
||||
sp.name,
|
||||
sp.name_en,
|
||||
sp.combo_name,
|
||||
COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name,
|
||||
sp.price,
|
||||
sp.market_price,
|
||||
sp.pic_url,
|
||||
@@ -141,7 +142,7 @@
|
||||
sp.id AS spu_id,
|
||||
sp.name,
|
||||
sp.name_en,
|
||||
sp.combo_name,
|
||||
COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name,
|
||||
sp.price,
|
||||
sp.market_price,
|
||||
sp.pic_url,
|
||||
@@ -184,7 +185,7 @@
|
||||
sp.id AS spu_id,
|
||||
sp.name,
|
||||
sp.name_en,
|
||||
sp.combo_name,
|
||||
COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name,
|
||||
sp.price,
|
||||
sp.market_price,
|
||||
sp.pic_url,
|
||||
@@ -232,7 +233,7 @@
|
||||
s.id as spu_id,
|
||||
s.`name`,
|
||||
s.name_en,
|
||||
s.combo_name,
|
||||
COALESCE(NULLIF(TRIM(m.display_name), ''), NULLIF(TRIM(s.combo_name), '')) AS combo_name,
|
||||
s.price,
|
||||
IFNULL(m.online_stock, 600) as stock,
|
||||
IFNULL(m.online_stock, 600) as online_stock,
|
||||
@@ -274,7 +275,7 @@
|
||||
s.id AS spu_id,
|
||||
s.name,
|
||||
s.name_en,
|
||||
s.combo_name,
|
||||
COALESCE(NULLIF(TRIM(m.display_name), ''), NULLIF(TRIM(s.combo_name), '')) AS combo_name,
|
||||
s.price,
|
||||
s.market_price,
|
||||
IFNULL(m.online_stock, 600) AS stock,
|
||||
@@ -309,7 +310,8 @@
|
||||
t.merchant_id,
|
||||
sp.id AS spu_id,
|
||||
sp.name,
|
||||
sp.combo_name,
|
||||
COALESCE(NULLIF(TRIM(t.display_name), ''), NULLIF(TRIM(sp.combo_name), '')) AS combo_name,
|
||||
t.display_name AS display_name,
|
||||
sp.introduction,
|
||||
sp.price,
|
||||
sp.market_price,
|
||||
@@ -350,6 +352,12 @@
|
||||
sold 可能小于恢复量,退款/补偿不能被统计口径阻断),配额为 0 等脏数据同样不得阻断;
|
||||
负增量扣减保留原「不扣成负销量、不超配额」边界,语义不变。
|
||||
-->
|
||||
<!--
|
||||
共享总库存:offline_stock=总库存,online_stock=移动端上限。
|
||||
delta 为库存协议增量(扣减为负):sold = sold - delta。
|
||||
移动端剩余 = LEAST(上限−已订, 总库存−已订−已售);
|
||||
AD stock / expected = 总库存−已订;扣减边界仍为 已订+已售 ≤ 总库存。
|
||||
-->
|
||||
<update id="updateOnlineStockDelta">
|
||||
UPDATE canteen_daily_menu
|
||||
SET online_sold = GREATEST(IFNULL(online_sold, 0) - #{delta}, 0)
|
||||
@@ -361,9 +369,13 @@
|
||||
<if test="delta <= 0">
|
||||
AND IFNULL(online_sold, 0) - #{delta} >= 0
|
||||
AND IFNULL(online_sold, 0) - #{delta} <= IFNULL(online_stock, 600)
|
||||
AND IFNULL(online_sold, 0) + IFNULL(offline_sold, 0) - #{delta} <= IFNULL(offline_stock, 600)
|
||||
</if>
|
||||
<if test="expected != null">
|
||||
AND (IFNULL(online_stock, 600) - IFNULL(online_sold, 0)) = #{expected}
|
||||
AND LEAST(
|
||||
GREATEST(IFNULL(online_stock, 600) - IFNULL(online_sold, 0), 0),
|
||||
GREATEST(IFNULL(offline_stock, 600) - IFNULL(online_sold, 0) - IFNULL(offline_sold, 0), 0)
|
||||
) = #{expected}
|
||||
</if>
|
||||
</update>
|
||||
|
||||
@@ -377,10 +389,10 @@
|
||||
AND spu_id = #{spuId}
|
||||
<if test="delta <= 0">
|
||||
AND IFNULL(offline_sold, 0) - #{delta} >= 0
|
||||
AND IFNULL(offline_sold, 0) - #{delta} <= IFNULL(offline_stock, 600)
|
||||
AND IFNULL(online_sold, 0) + IFNULL(offline_sold, 0) - #{delta} <= IFNULL(offline_stock, 600)
|
||||
</if>
|
||||
<if test="expected != null">
|
||||
AND (IFNULL(offline_stock, 600) - IFNULL(offline_sold, 0)) = #{expected}
|
||||
AND GREATEST(IFNULL(offline_stock, 600) - IFNULL(online_sold, 0), 0) = #{expected}
|
||||
</if>
|
||||
</update>
|
||||
</mapper>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.bsd.pjcloud.supermarket.constant;
|
||||
|
||||
/**
|
||||
* 酒店数据来源(存 canteen_hotel.sheet_name,字典类型 {@link #DICT_TYPE})。
|
||||
*/
|
||||
public final class SmHotelDataSource {
|
||||
|
||||
/** 字典类型 */
|
||||
public static final String DICT_TYPE = "sm_hotel_data_source";
|
||||
|
||||
/** 本地手工新增 */
|
||||
public static final String LOCAL = "local";
|
||||
|
||||
/** 商游上游同步/导入 */
|
||||
public static final String SHANGYOU = "shangyou";
|
||||
|
||||
/** 同步导入时暂存所选来源(租户 Redis 自动加前缀) */
|
||||
public static final String REDIS_IMPORT_DATA_SOURCE = "supermarket:shangyou:hotel:import:dataSource";
|
||||
|
||||
private SmHotelDataSource() {
|
||||
}
|
||||
|
||||
public static boolean isLocal(String dataSource) {
|
||||
return LOCAL.equalsIgnoreCase(dataSource == null ? null : dataSource.trim());
|
||||
}
|
||||
}
|
||||
+27
-1
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@@ -42,7 +43,11 @@ public class SmHotelEntity extends Model<SmHotelEntity> {
|
||||
@Schema(description = "酒店名称")
|
||||
private String hotelName;
|
||||
|
||||
@Schema(description = "导入来源Sheet名")
|
||||
/**
|
||||
* 数据来源(库字段 sheet_name)。存字典 {@code sm_hotel_data_source} 的 item_value,
|
||||
* 如 local=本地、shangyou=商游上游;历史 Excel Sheet 名仍可能原样存于此列。
|
||||
*/
|
||||
@Schema(description = "数据来源(字典 sm_hotel_data_source)")
|
||||
private String sheetName;
|
||||
|
||||
@Schema(description = "ACTIVE启用 / OFFLINE上游下线停用")
|
||||
@@ -51,6 +56,27 @@ public class SmHotelEntity extends Model<SmHotelEntity> {
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "酒店主图")
|
||||
private String coverImage;
|
||||
|
||||
@Schema(description = "轮播图URL,逗号分隔")
|
||||
private String carouselImages;
|
||||
|
||||
@Schema(description = "详细地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "经度")
|
||||
private BigDecimal longitude;
|
||||
|
||||
@Schema(description = "纬度")
|
||||
private BigDecimal latitude;
|
||||
|
||||
@Schema(description = "联系电话")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "酒店简介")
|
||||
private String intro;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
|
||||
+6
@@ -133,6 +133,12 @@ public class ShangyouHotelProperties {
|
||||
/** 列表同步后是否自动把「上游有本地无」写入本地主数据(会按批循环导完差集) */
|
||||
private boolean autoImportNewHotels = true;
|
||||
|
||||
/**
|
||||
* 定时/XXL 列表同步自动导入时的默认数据来源(字典 sm_hotel_data_source)。
|
||||
* 人工同步须在前端显式选择;定时任务用此配置。
|
||||
*/
|
||||
private String defaultImportDataSource = "shangyou";
|
||||
|
||||
/**
|
||||
* 自动导入时每批条数(控内存/事务体积)。同步任务会循环多批直到差集清空,不是「总共只导这么多」。
|
||||
*/
|
||||
|
||||
+2
-2
@@ -61,7 +61,7 @@ public class SmHotelCallbackTaskController {
|
||||
|
||||
@SysLog("按时间区间删除酒店回调任务")
|
||||
@Operation(summary = "按创建时间区间删除回调任务")
|
||||
@DeleteMapping("/range")
|
||||
@PostMapping("/range")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelCallback_del')")
|
||||
public R<Integer> removeRange(@RequestParam(required = false) String createTimeBegin,
|
||||
@RequestParam(required = false) String createTimeEnd) {
|
||||
@@ -70,7 +70,7 @@ public class SmHotelCallbackTaskController {
|
||||
|
||||
@SysLog("清空酒店回调任务")
|
||||
@Operation(summary = "清空全部回调任务并清理 Redis 队列")
|
||||
@DeleteMapping("/clear")
|
||||
@PostMapping("/clear")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelCallback_del')")
|
||||
public R<Integer> clearAll() {
|
||||
return callbackTaskService.clearAllTasks();
|
||||
|
||||
+2
-2
@@ -75,10 +75,10 @@ public class SmHotelController {
|
||||
}
|
||||
|
||||
@SysLog("新增酒店")
|
||||
@Operation(summary = "新增")
|
||||
@Operation(summary = "新增(返回已落库实体,便于继续维护房型等)")
|
||||
@PostMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_add')")
|
||||
public R save(@RequestBody SmHotelEntity entity) {
|
||||
public R<SmHotelEntity> save(@RequestBody SmHotelEntity entity) {
|
||||
return R.ok(hotelService.saveHotel(entity));
|
||||
}
|
||||
|
||||
|
||||
+58
-9
@@ -3,6 +3,8 @@ package com.bsd.pjcloud.supermarket.controller;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelPriceConfigEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelSyncLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
|
||||
@@ -135,20 +137,23 @@ public class SmHotelMidController {
|
||||
}
|
||||
|
||||
@SysLog("导入上游授权酒店到本地")
|
||||
@Operation(summary = "将上游授权酒店写入本地")
|
||||
@Operation(summary = "将上游授权酒店写入本地(须选数据来源)")
|
||||
@PostMapping("/mid/authorizedHotels/import")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_add')")
|
||||
public R<Map<String, Object>> importAuthorizedHotels(@RequestBody List<Map<String, Object>> hotels) {
|
||||
return midService.importAuthorizedHotels(hotels);
|
||||
public R<Map<String, Object>> importAuthorizedHotels(
|
||||
@RequestParam("dataSource") String dataSource,
|
||||
@RequestBody List<Map<String, Object>> hotels) {
|
||||
return midService.importAuthorizedHotels(hotels, dataSource);
|
||||
}
|
||||
|
||||
@SysLog("同步上游酒店列表并差集")
|
||||
@Operation(summary = "异步同步 getHotelList + Redis 差集(立刻返回 logId,轮询进度)")
|
||||
@Operation(summary = "异步同步 getHotelList + Redis 差集(立刻返回 logId;须选数据来源)")
|
||||
@PostMapping("/mid/syncHotelList")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Map<String, Object>> syncHotelList(
|
||||
@RequestParam(required = false, defaultValue = "true") boolean autoImport) {
|
||||
return syncService.syncHotelListAndDiff(autoImport);
|
||||
@RequestParam(required = false, defaultValue = "true") boolean autoImport,
|
||||
@RequestParam("dataSource") String dataSource) {
|
||||
return syncService.syncHotelListAndDiff(autoImport, dataSource);
|
||||
}
|
||||
|
||||
@Operation(summary = "同步日志分页")
|
||||
@@ -199,11 +204,55 @@ public class SmHotelMidController {
|
||||
}
|
||||
|
||||
@SysLog("导入差集新增酒店")
|
||||
@Operation(summary = "将差集中的新增酒店导入本地")
|
||||
@Operation(summary = "将差集中的新增酒店导入本地(dataSource 空则用同步时暂存的来源)")
|
||||
@PostMapping("/mid/hotelDiff/importNew")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_add')")
|
||||
public R<Map<String, Object>> importDiffNew(@RequestParam(required = false) Integer limit) {
|
||||
return syncService.importDiffNewHotels(limit);
|
||||
public R<Map<String, Object>> importDiffNew(@RequestParam(required = false) Integer limit,
|
||||
@RequestParam(required = false) String dataSource) {
|
||||
return syncService.importDiffNewHotels(limit, dataSource);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店保存房型")
|
||||
@PostMapping("/local/{hotelId}/room")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<SmHotelRoomEntity> saveLocalRoom(@PathVariable Long hotelId, @RequestBody SmHotelRoomEntity room) {
|
||||
return midService.saveLocalRoom(hotelId, room);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店删除房型")
|
||||
@DeleteMapping("/local/{hotelId}/room/{roomId}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Boolean> removeLocalRoom(@PathVariable Long hotelId, @PathVariable Long roomId) {
|
||||
return midService.removeLocalRoom(hotelId, roomId);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店保存产品")
|
||||
@PostMapping("/local/{hotelId}/product")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<SmHotelProductEntity> saveLocalProduct(@PathVariable Long hotelId,
|
||||
@RequestBody SmHotelProductEntity product) {
|
||||
return midService.saveLocalProduct(hotelId, product);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店删除产品")
|
||||
@DeleteMapping("/local/{hotelId}/product/{productId}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Boolean> removeLocalProduct(@PathVariable Long hotelId, @PathVariable Long productId) {
|
||||
return midService.removeLocalProduct(hotelId, productId);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店保存日历")
|
||||
@PostMapping("/local/{hotelId}/daily")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<SmHotelDailyEntity> saveLocalDaily(@PathVariable Long hotelId, @RequestBody SmHotelDailyEntity daily) {
|
||||
return midService.saveLocalDaily(hotelId, daily);
|
||||
}
|
||||
|
||||
@SysLog("本地酒店删除日历")
|
||||
@DeleteMapping("/local/{hotelId}/daily/{dailyId}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Boolean> removeLocalDaily(@PathVariable Long hotelId, @PathVariable Long dailyId) {
|
||||
return midService.removeLocalDaily(hotelId, dailyId);
|
||||
}
|
||||
|
||||
@SysLog("手动执行滚一天房价")
|
||||
|
||||
+26
-15
@@ -7,11 +7,11 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -65,19 +65,8 @@ public class ShangyouCallbackQueueWorker implements ApplicationRunner {
|
||||
int consecutiveFail = 0;
|
||||
while (running.get()) {
|
||||
try {
|
||||
// BRPOP 多 key:先高优(订单状态)再普通;阻塞期间新到的高优也会被优先弹出
|
||||
// ListOperations 没有多 key 阻塞弹出,走底层连接命令 bRPop(秒, keys...),返回 [key, value]
|
||||
// lambda 参数必须显式声明 RedisConnection,否则 execute 在 RedisCallback/SessionCallback 两个重载间产生歧义
|
||||
String taskId = redisTemplate.execute((RedisConnection connection) -> {
|
||||
List<byte[]> popped = connection.listCommands().bRPop(5,
|
||||
ShangyouHotelApi.REDIS_CALLBACK_QUEUE_HIGH.getBytes(StandardCharsets.UTF_8),
|
||||
ShangyouHotelApi.REDIS_CALLBACK_QUEUE.getBytes(StandardCharsets.UTF_8));
|
||||
return popped == null || popped.size() < 2 ? null
|
||||
: new String(popped.get(1), StandardCharsets.UTF_8);
|
||||
});
|
||||
if (taskId != null) {
|
||||
taskId = taskId.trim();
|
||||
}
|
||||
// 底层 BRPOP 多 key:先高优再普通(ListOperations 无多 key 重载)
|
||||
String taskId = brpopPreferHigh(5);
|
||||
if (taskId == null || taskId.isBlank() || "null".equalsIgnoreCase(taskId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -130,6 +119,28 @@ public class ShangyouCallbackQueueWorker implements ApplicationRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis BRPOP:按 key 顺序优先弹出高优队列。
|
||||
* 当前 Spring Data Redis 的 {@code ListOperations#rightPop} 不支持多 key,走 connection 原生命令。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String brpopPreferHigh(int timeoutSeconds) {
|
||||
RedisSerializer<String> keySer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
|
||||
RedisSerializer<String> valSer = (RedisSerializer<String>) redisTemplate.getValueSerializer();
|
||||
byte[] highKey = keySer.serialize(ShangyouHotelApi.REDIS_CALLBACK_QUEUE_HIGH);
|
||||
byte[] normalKey = keySer.serialize(ShangyouHotelApi.REDIS_CALLBACK_QUEUE);
|
||||
if (highKey == null || normalKey == null) {
|
||||
return null;
|
||||
}
|
||||
List<byte[]> popped = redisTemplate.execute((RedisCallback<List<byte[]>>) connection ->
|
||||
connection.listCommands().bRPop(timeoutSeconds, highKey, normalKey));
|
||||
if (popped == null || popped.size() < 2 || popped.get(1) == null) {
|
||||
return null;
|
||||
}
|
||||
String taskId = valSer.deserialize(popped.get(1));
|
||||
return taskId == null ? null : taskId.trim();
|
||||
}
|
||||
|
||||
/** 不依赖 Worker 空闲:定时扫卡死的 RUNNING */
|
||||
private void watchdogLoop() {
|
||||
while (running.get()) {
|
||||
|
||||
+3
@@ -59,6 +59,9 @@ public final class ShangyouHotelApi {
|
||||
/** 差集:本地有上游无(疑似下线) */
|
||||
public static final String REDIS_DIFF_OFFLINE_SET = "supermarket:shangyou:hotel:diff:offline:v1";
|
||||
|
||||
/** 列表同步/差集导入所选数据来源(字典值) */
|
||||
public static final String REDIS_IMPORT_DATA_SOURCE = "supermarket:shangyou:hotel:import:dataSource";
|
||||
|
||||
/** 今日全量房价同步次数 */
|
||||
public static final String REDIS_FULL_PRICE_SYNC_COUNT = "supermarket:shangyou:hotel:fullprice:count";
|
||||
|
||||
|
||||
+25
-2
@@ -2,6 +2,8 @@ package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelDetailVO;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
|
||||
@@ -56,6 +58,27 @@ public interface SmHotelMidService {
|
||||
/** 重建本地 hotel_code Redis SET(分批,防 OOM);供差集与列表标记复用 */
|
||||
long rebuildLocalHotelCodeSet();
|
||||
|
||||
/** 将选中的上游 hotelId 写入本地(不存在则新增) */
|
||||
R<Map<String, Object>> importAuthorizedHotels(List<Map<String, Object>> hotels);
|
||||
/**
|
||||
* 将选中的上游 hotelId 写入本地(不存在则新增)。
|
||||
* @param dataSource 数据来源字典值(必填,不可为 local)
|
||||
*/
|
||||
R<Map<String, Object>> importAuthorizedHotels(List<Map<String, Object>> hotels, String dataSource);
|
||||
|
||||
/** 本地数据来源:保存房型 */
|
||||
R<SmHotelRoomEntity> saveLocalRoom(Long hotelId, SmHotelRoomEntity room);
|
||||
|
||||
/** 本地数据来源:删除房型 */
|
||||
R<Boolean> removeLocalRoom(Long hotelId, Long roomId);
|
||||
|
||||
/** 本地数据来源:保存产品 */
|
||||
R<SmHotelProductEntity> saveLocalProduct(Long hotelId, SmHotelProductEntity product);
|
||||
|
||||
/** 本地数据来源:删除产品 */
|
||||
R<Boolean> removeLocalProduct(Long hotelId, Long productId);
|
||||
|
||||
/** 本地数据来源:保存日历行 */
|
||||
R<SmHotelDailyEntity> saveLocalDaily(Long hotelId, SmHotelDailyEntity daily);
|
||||
|
||||
/** 本地数据来源:删除日历行 */
|
||||
R<Boolean> removeLocalDaily(Long hotelId, Long dailyId);
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@ public interface SmHotelService extends IService<SmHotelEntity> {
|
||||
|
||||
Page<SmHotelEntity> pageScope(Page<SmHotelEntity> page, LambdaQueryWrapper<SmHotelEntity> wrapper);
|
||||
|
||||
Boolean saveHotel(SmHotelEntity entity);
|
||||
SmHotelEntity saveHotel(SmHotelEntity entity);
|
||||
|
||||
Boolean updateHotel(SmHotelEntity entity);
|
||||
|
||||
|
||||
+9
-5
@@ -14,9 +14,10 @@ public interface SmHotelSyncService {
|
||||
/**
|
||||
* 提交列表同步(立刻返回 logId,后台异步执行)。
|
||||
* @param autoImport 是否自动导入新增
|
||||
* @param dataSource 自动导入时写入的数据来源(字典值);空则用配置 defaultImportDataSource
|
||||
* @param triggerType MANUAL / XXL / CRON
|
||||
*/
|
||||
R<SmHotelSyncLogEntity> startListSyncAsync(boolean autoImport, String triggerType);
|
||||
R<SmHotelSyncLogEntity> startListSyncAsync(boolean autoImport, String dataSource, String triggerType);
|
||||
|
||||
/** 查询同步任务进度/结果 */
|
||||
R<SmHotelSyncLogEntity> getSyncLog(Long id);
|
||||
@@ -27,8 +28,11 @@ public interface SmHotelSyncService {
|
||||
/** 分页查看差集:type=new|offline */
|
||||
R<Map<String, Object>> pageHotelDiff(String type, long current, long size);
|
||||
|
||||
/** 把差集「新增」全部/限额导入本地主数据 */
|
||||
R<Map<String, Object>> importDiffNewHotels(Integer limit);
|
||||
/**
|
||||
* 把差集「新增」全部/限额导入本地主数据。
|
||||
* @param dataSource 数据来源字典值;空则读 Redis 中同步时暂存的来源
|
||||
*/
|
||||
R<Map<String, Object>> importDiffNewHotels(Integer limit, String dataSource);
|
||||
|
||||
/** 提交滚价异步任务 */
|
||||
R<SmHotelSyncLogEntity> startPriceRollAsync(String triggerType);
|
||||
@@ -53,8 +57,8 @@ public interface SmHotelSyncService {
|
||||
*/
|
||||
int abortStaleRunningSyncLogs();
|
||||
|
||||
/** 兼容旧同步入口(内部转异步) */
|
||||
R<Map<String, Object>> syncHotelListAndDiff(boolean autoImport);
|
||||
/** 兼容旧同步入口(内部转异步);人工同步须传 dataSource */
|
||||
R<Map<String, Object>> syncHotelListAndDiff(boolean autoImport, String dataSource);
|
||||
|
||||
/** 当前日更时间配置(给前端展示) */
|
||||
Map<String, Object> getSyncSchedule();
|
||||
|
||||
+187
-2
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
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.entity.*;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelClient;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
@@ -603,10 +604,17 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Map<String, Object>> importAuthorizedHotels(List<Map<String, Object>> hotels) {
|
||||
public R<Map<String, Object>> importAuthorizedHotels(List<Map<String, Object>> hotels, String dataSource) {
|
||||
if (CollUtil.isEmpty(hotels)) {
|
||||
return R.failed("请选择要导入的酒店");
|
||||
}
|
||||
String source = StrUtil.trim(dataSource);
|
||||
if (StrUtil.isBlank(source)) {
|
||||
return R.failed("请选择数据来源");
|
||||
}
|
||||
if (SmHotelDataSource.isLocal(source)) {
|
||||
return R.failed("上游同步不能使用「本地」数据来源,请选择上游类来源");
|
||||
}
|
||||
int insert = 0;
|
||||
int skip = 0;
|
||||
for (Map<String, Object> row : hotels) {
|
||||
@@ -625,7 +633,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
SmHotelEntity hotel = new SmHotelEntity();
|
||||
hotel.setHotelCode(hotelId);
|
||||
hotel.setHotelName(StrUtil.blankToDefault(hotelName, hotelId));
|
||||
hotel.setSheetName("上游授权列表");
|
||||
hotel.setSheetName(source);
|
||||
hotel.setUpstreamStatus("ACTIVE");
|
||||
hotelMapper.insert(hotel);
|
||||
stringRedisTemplate.opsForSet().add(ShangyouHotelApi.REDIS_LOCAL_CODE_SET, hotelId);
|
||||
@@ -634,9 +642,186 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
Map<String, Object> result = new HashMap<>(4);
|
||||
result.put("insertCount", insert);
|
||||
result.put("skipCount", skip);
|
||||
result.put("dataSource", source);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelRoomEntity> saveLocalRoom(Long hotelId, SmHotelRoomEntity room) {
|
||||
SmHotelEntity hotel = requireLocalHotel(hotelId);
|
||||
if (room == null || StrUtil.isBlank(room.getRoomTypeCode())) {
|
||||
return R.failed("房型编码不能为空");
|
||||
}
|
||||
room.setHotelId(hotel.getId());
|
||||
room.setHotelCode(hotel.getHotelCode());
|
||||
room.setGroupCode(hotel.getGroupCode());
|
||||
if (room.getId() == null) {
|
||||
Long exists = roomMapper.selectCount(Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
.eq(SmHotelRoomEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelRoomEntity::getRoomTypeCode, room.getRoomTypeCode().trim()));
|
||||
if (exists != null && exists > 0) {
|
||||
return R.failed("房型编码已存在:" + room.getRoomTypeCode());
|
||||
}
|
||||
room.setRoomTypeCode(room.getRoomTypeCode().trim());
|
||||
roomMapper.insert(room);
|
||||
}
|
||||
else {
|
||||
SmHotelRoomEntity old = roomMapper.selectById(room.getId());
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
return R.failed("房型不存在");
|
||||
}
|
||||
room.setRoomTypeCode(room.getRoomTypeCode().trim());
|
||||
roomMapper.updateById(room);
|
||||
}
|
||||
return R.ok(roomMapper.selectById(room.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> removeLocalRoom(Long hotelId, Long roomId) {
|
||||
requireLocalHotel(hotelId);
|
||||
SmHotelRoomEntity old = roomMapper.selectById(roomId);
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
return R.failed("房型不存在");
|
||||
}
|
||||
Long productCnt = productMapper.selectCount(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelProductEntity::getRoomTypeCode, old.getRoomTypeCode()));
|
||||
if (productCnt != null && productCnt > 0) {
|
||||
return R.failed("请先删除该房型下的产品");
|
||||
}
|
||||
return R.ok(roomMapper.deleteById(roomId) > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelProductEntity> saveLocalProduct(Long hotelId, SmHotelProductEntity product) {
|
||||
SmHotelEntity hotel = requireLocalHotel(hotelId);
|
||||
if (product == null || StrUtil.isBlank(product.getProductCode())) {
|
||||
return R.failed("产品编码不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(product.getRoomTypeCode())) {
|
||||
return R.failed("所属房型编码不能为空");
|
||||
}
|
||||
Long roomCnt = roomMapper.selectCount(Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
.eq(SmHotelRoomEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelRoomEntity::getRoomTypeCode, product.getRoomTypeCode().trim()));
|
||||
if (roomCnt == null || roomCnt <= 0) {
|
||||
return R.failed("所属房型不存在,请先维护房型");
|
||||
}
|
||||
product.setHotelId(hotel.getId());
|
||||
product.setHotelCode(hotel.getHotelCode());
|
||||
product.setGroupCode(hotel.getGroupCode());
|
||||
product.setProductCode(product.getProductCode().trim());
|
||||
product.setRoomTypeCode(product.getRoomTypeCode().trim());
|
||||
SmHotelRoomEntity room = roomMapper.selectOne(Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
.eq(SmHotelRoomEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelRoomEntity::getRoomTypeCode, product.getRoomTypeCode())
|
||||
.last("LIMIT 1"));
|
||||
if (room != null) {
|
||||
product.setRoomId(room.getId());
|
||||
}
|
||||
if (product.getId() == null) {
|
||||
Long exists = productMapper.selectCount(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelProductEntity::getProductCode, product.getProductCode()));
|
||||
if (exists != null && exists > 0) {
|
||||
return R.failed("产品编码已存在:" + product.getProductCode());
|
||||
}
|
||||
productMapper.insert(product);
|
||||
}
|
||||
else {
|
||||
SmHotelProductEntity old = productMapper.selectById(product.getId());
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
return R.failed("产品不存在");
|
||||
}
|
||||
productMapper.updateById(product);
|
||||
}
|
||||
return R.ok(productMapper.selectById(product.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> removeLocalProduct(Long hotelId, Long productId) {
|
||||
requireLocalHotel(hotelId);
|
||||
SmHotelProductEntity old = productMapper.selectById(productId);
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
return R.failed("产品不存在");
|
||||
}
|
||||
dailyMapper.delete(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelDailyEntity::getProductId, productId));
|
||||
return R.ok(productMapper.deleteById(productId) > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelDailyEntity> saveLocalDaily(Long hotelId, SmHotelDailyEntity daily) {
|
||||
SmHotelEntity hotel = requireLocalHotel(hotelId);
|
||||
if (daily == null || daily.getStayDate() == null) {
|
||||
return R.failed("入住日期不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(daily.getProductCode())) {
|
||||
return R.failed("产品编码不能为空");
|
||||
}
|
||||
SmHotelProductEntity product = productMapper.selectOne(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelProductEntity::getProductCode, daily.getProductCode().trim())
|
||||
.last("LIMIT 1"));
|
||||
if (product == null) {
|
||||
return R.failed("产品不存在,请先维护产品");
|
||||
}
|
||||
daily.setHotelId(hotel.getId());
|
||||
daily.setProductId(product.getId());
|
||||
daily.setProductCode(product.getProductCode());
|
||||
daily.setRoomTypeCode(product.getRoomTypeCode());
|
||||
if (daily.getDailyStatus() == null) {
|
||||
daily.setDailyStatus(1);
|
||||
}
|
||||
if (daily.getId() == null) {
|
||||
SmHotelDailyEntity exist = dailyMapper.selectOne(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelDailyEntity::getProductCode, daily.getProductCode())
|
||||
.eq(SmHotelDailyEntity::getStayDate, daily.getStayDate())
|
||||
.last("LIMIT 1"));
|
||||
if (exist != null) {
|
||||
daily.setId(exist.getId());
|
||||
dailyMapper.updateById(daily);
|
||||
}
|
||||
else {
|
||||
dailyMapper.insert(daily);
|
||||
}
|
||||
}
|
||||
else {
|
||||
SmHotelDailyEntity old = dailyMapper.selectById(daily.getId());
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
return R.failed("日历行不存在");
|
||||
}
|
||||
dailyMapper.updateById(daily);
|
||||
}
|
||||
return R.ok(dailyMapper.selectById(daily.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> removeLocalDaily(Long hotelId, Long dailyId) {
|
||||
requireLocalHotel(hotelId);
|
||||
SmHotelDailyEntity old = dailyMapper.selectById(dailyId);
|
||||
if (old == null || !Objects.equals(old.getHotelId(), hotelId)) {
|
||||
return R.failed("日历行不存在");
|
||||
}
|
||||
return R.ok(dailyMapper.deleteById(dailyId) > 0);
|
||||
}
|
||||
|
||||
private SmHotelEntity requireLocalHotel(Long hotelId) {
|
||||
SmHotelEntity hotel = requireHotel(hotelId);
|
||||
if (!SmHotelDataSource.isLocal(hotel.getSheetName())) {
|
||||
throw new IllegalArgumentException("仅「本地」数据来源的酒店可手工维护房型/产品/日历");
|
||||
}
|
||||
return hotel;
|
||||
}
|
||||
|
||||
private SmHotelEntity requireHotel(Long hotelId) {
|
||||
if (hotelId == null) {
|
||||
throw new IllegalArgumentException("酒店ID不能为空");
|
||||
|
||||
+20
-2
@@ -14,6 +14,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDailyMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
|
||||
@@ -111,7 +112,7 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean saveHotel(SmHotelEntity entity) {
|
||||
public SmHotelEntity saveHotel(SmHotelEntity entity) {
|
||||
normalize(entity);
|
||||
if (StrUtil.isBlank(entity.getHotelName()) && StrUtil.isBlank(entity.getHotelCode())) {
|
||||
throw new IllegalArgumentException("酒店名称与酒店编码不能同时为空");
|
||||
@@ -119,10 +120,18 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
if (StrUtil.isNotBlank(entity.getHotelCode()) && existsHotelCode(entity.getHotelCode(), null)) {
|
||||
throw new IllegalArgumentException("酒店编码已存在:" + entity.getHotelCode());
|
||||
}
|
||||
// 手工新增默认数据来源=本地(字典 sm_hotel_data_source.local)
|
||||
if (StrUtil.isBlank(entity.getSheetName())) {
|
||||
entity.setSheetName(SmHotelDataSource.LOCAL);
|
||||
}
|
||||
if (StrUtil.isBlank(entity.getUpstreamStatus())) {
|
||||
entity.setUpstreamStatus("ACTIVE");
|
||||
}
|
||||
if (entity.getDeptId() == null) {
|
||||
entity.setDeptId(SecurityUtils.getUser().getDeptId());
|
||||
}
|
||||
return baseMapper.insert(entity) > 0;
|
||||
baseMapper.insert(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,6 +151,10 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
if (StrUtil.isNotBlank(entity.getHotelCode()) && existsHotelCode(entity.getHotelCode(), entity.getId())) {
|
||||
throw new IllegalArgumentException("酒店编码已存在:" + entity.getHotelCode());
|
||||
}
|
||||
// 未传数据来源时保留原值,避免覆盖
|
||||
if (StrUtil.isBlank(entity.getSheetName())) {
|
||||
entity.setSheetName(old.getSheetName());
|
||||
}
|
||||
return baseMapper.updateById(entity) > 0;
|
||||
}
|
||||
|
||||
@@ -378,6 +391,11 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
|
||||
entity.setHotelName(trimToNull(entity.getHotelName()));
|
||||
entity.setSheetName(trimToNull(entity.getSheetName()));
|
||||
entity.setRemark(trimToNull(entity.getRemark()));
|
||||
entity.setCoverImage(trimToNull(entity.getCoverImage()));
|
||||
entity.setCarouselImages(trimToNull(entity.getCarouselImages()));
|
||||
entity.setAddress(trimToNull(entity.getAddress()));
|
||||
entity.setPhone(trimToNull(entity.getPhone()));
|
||||
entity.setIntro(trimToNull(entity.getIntro()));
|
||||
}
|
||||
|
||||
private static String trimToNull(String v) {
|
||||
|
||||
+47
-12
@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
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.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelSyncLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
@@ -76,7 +77,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
@Override
|
||||
public R<SmHotelSyncLogEntity> startListSyncAsync(boolean autoImport, String triggerType) {
|
||||
public R<SmHotelSyncLogEntity> startListSyncAsync(boolean autoImport, String dataSource, String triggerType) {
|
||||
abortStaleRunningSyncLogs();
|
||||
Long running = syncLogMapper.selectCount(Wrappers.<SmHotelSyncLogEntity>lambdaQuery()
|
||||
.eq(SmHotelSyncLogEntity::getSyncType, TYPE_LIST)
|
||||
@@ -84,9 +85,19 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
if (running != null && running > 0) {
|
||||
return R.failed("已有列表同步任务进行中,请稍候查看进度(若长时间卡住请在同步日志点「终止」)");
|
||||
}
|
||||
String source = resolveImportDataSource(dataSource, triggerType);
|
||||
if (StrUtil.isBlank(source)) {
|
||||
return R.failed("请选择数据来源(字典 sm_hotel_data_source)");
|
||||
}
|
||||
if (SmHotelDataSource.isLocal(source)) {
|
||||
return R.failed("上游同步不能使用「本地」数据来源");
|
||||
}
|
||||
// 供差集「补导剩余新增」复用本次选择的来源
|
||||
redisTemplate.opsForValue().set(ShangyouHotelApi.REDIS_IMPORT_DATA_SOURCE, source, 2, TimeUnit.DAYS);
|
||||
|
||||
SmHotelSyncLogEntity logEntity = newLog(TYPE_LIST, triggerType);
|
||||
logEntity.setStage("QUEUED");
|
||||
logEntity.setStageMsg("已提交,等待执行…");
|
||||
logEntity.setStageMsg("已提交,数据来源=" + source + ",等待执行…");
|
||||
logEntity.setProgressPct(1);
|
||||
syncLogMapper.insert(logEntity);
|
||||
|
||||
@@ -97,7 +108,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
final Long tid = tenantId;
|
||||
final Long logId = logEntity.getId();
|
||||
final boolean doImport = autoImport;
|
||||
EXECUTOR.execute(() -> TenantBroker.runAs(tid, id -> runListSync(logId, doImport)));
|
||||
final String importSource = source;
|
||||
EXECUTOR.execute(() -> TenantBroker.runAs(tid, id -> runListSync(logId, doImport, importSource)));
|
||||
return R.ok(logEntity, "已提交异步同步");
|
||||
}
|
||||
|
||||
@@ -274,8 +286,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Map<String, Object>> syncHotelListAndDiff(boolean autoImport) {
|
||||
R<SmHotelSyncLogEntity> started = startListSyncAsync(autoImport, "MANUAL");
|
||||
public R<Map<String, Object>> syncHotelListAndDiff(boolean autoImport, String dataSource) {
|
||||
R<SmHotelSyncLogEntity> started = startListSyncAsync(autoImport, dataSource, "MANUAL");
|
||||
if (started.getCode() != 0) {
|
||||
return R.failed(started.getMsg());
|
||||
}
|
||||
@@ -283,11 +295,12 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
data.put("async", true);
|
||||
data.put("logId", started.getData().getId());
|
||||
data.put("status", started.getData().getStatus());
|
||||
data.put("dataSource", StrUtil.trim(dataSource));
|
||||
data.put("message", "已异步执行,请轮询 /mid/syncLog/{id} 或打开同步日志");
|
||||
return R.ok(data, started.getMsg());
|
||||
}
|
||||
|
||||
private void runListSync(Long logId, boolean autoImport) {
|
||||
private void runListSync(Long logId, boolean autoImport, String dataSource) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
progress(logId, 5, "REFRESH_LIST", "正在拉取上游授权酒店列表…");
|
||||
@@ -318,9 +331,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
}
|
||||
int pct = Math.min(78, 55 + round);
|
||||
progress(logId, pct, "IMPORT_NEW",
|
||||
"正在自动导入新增酒店…已导入 " + imported + ",剩余约 " + remain
|
||||
+ "(每批 " + batchSize + ")");
|
||||
R<Map<String, Object>> imp = importDiffNewHotels(batchSize);
|
||||
"正在自动导入新增酒店(来源 " + dataSource + ")…已导入 " + imported + ",剩余约 "
|
||||
+ remain + "(每批 " + batchSize + ")");
|
||||
R<Map<String, Object>> imp = importDiffNewHotels(batchSize, dataSource);
|
||||
int n = 0;
|
||||
if (imp != null && imp.getData() != null && imp.getData().get("insertCount") != null) {
|
||||
n = Integer.parseInt(String.valueOf(imp.getData().get("insertCount")));
|
||||
@@ -947,7 +960,17 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Map<String, Object>> importDiffNewHotels(Integer limit) {
|
||||
public R<Map<String, Object>> importDiffNewHotels(Integer limit, String dataSource) {
|
||||
String source = StrUtil.trim(dataSource);
|
||||
if (StrUtil.isBlank(source)) {
|
||||
source = redisTemplate.opsForValue().get(ShangyouHotelApi.REDIS_IMPORT_DATA_SOURCE);
|
||||
}
|
||||
if (StrUtil.isBlank(source)) {
|
||||
source = StrUtil.trim(properties.getDefaultImportDataSource());
|
||||
}
|
||||
if (StrUtil.isBlank(source)) {
|
||||
return R.failed("请选择数据来源后再导入");
|
||||
}
|
||||
int max = limit == null || limit <= 0 ? properties.getAutoImportBatchSize() : limit;
|
||||
max = Math.min(Math.max(max, 1), 5000);
|
||||
List<Map<String, Object>> batch = new ArrayList<>();
|
||||
@@ -973,7 +996,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
empty.put("skipCount", 0);
|
||||
return R.ok(empty, "无待导入酒店");
|
||||
}
|
||||
R<Map<String, Object>> r = midService.importAuthorizedHotels(batch);
|
||||
R<Map<String, Object>> r = midService.importAuthorizedHotels(batch, source);
|
||||
if (r != null && r.getCode() == 0) {
|
||||
for (Map<String, Object> row : batch) {
|
||||
Object id = row.get("hotelId");
|
||||
@@ -986,6 +1009,18 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 人工同步必须显式传;定时任务用配置 defaultImportDataSource */
|
||||
private String resolveImportDataSource(String dataSource, String triggerType) {
|
||||
String source = StrUtil.trim(dataSource);
|
||||
if (StrUtil.isNotBlank(source)) {
|
||||
return source;
|
||||
}
|
||||
if (!"MANUAL".equalsIgnoreCase(StrUtil.trim(triggerType))) {
|
||||
return StrUtil.trim(properties.getDefaultImportDataSource());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getSyncSchedule() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
@@ -1014,7 +1049,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return Map.of("skipped", true, "reason", "hotel-list-daily-sync-enabled=false");
|
||||
}
|
||||
R<SmHotelSyncLogEntity> r = startListSyncAsync(properties.isAutoImportNewHotels(),
|
||||
StrUtil.blankToDefault(triggerType, "XXL"));
|
||||
properties.getDefaultImportDataSource(), StrUtil.blankToDefault(triggerType, "XXL"));
|
||||
if (r.getCode() != 0) {
|
||||
return Map.of("ok", false, "msg", r.getMsg());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user