diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/api/feign/RemoteSmHotelFxInviteService.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/api/feign/RemoteSmHotelFxInviteService.java new file mode 100644 index 000000000..faf8a3f68 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/api/feign/RemoteSmHotelFxInviteService.java @@ -0,0 +1,28 @@ +package com.bsd.pjcloud.supermarket.api.feign; + +import com.bsd.pjcloud.common.core.constant.ServiceNameConstants; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.feign.annotation.NoToken; +import com.bsd.pjcloud.supermarket.vo.SmHotelFxInvitePreviewVO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 酒店分销邀请:UPMS 注册页识别分销邀请码后回调挂靠分销员。 + */ +@FeignClient(contextId = "remoteSmHotelFxInviteService", value = ServiceNameConstants.SUPERMARKET_SERVICE) +public interface RemoteSmHotelFxInviteService { + + @NoToken + @GetMapping("/inner/smHotelFx/invite/preview") + R preview(@RequestParam("inviteCode") String inviteCode); + + @NoToken + @PostMapping("/inner/smHotelFx/invite/bindAgent") + R bindAgent(@RequestParam("inviteCode") String inviteCode, @RequestParam("userId") Long userId, + @RequestParam(value = "username", required = false) String username, + @RequestParam(value = "realName", required = false) String realName, + @RequestParam(value = "mobile", required = false) String mobile); +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamProtocol.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamProtocol.java index 0e5262e6f..1a0dca25b 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamProtocol.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamProtocol.java @@ -17,9 +17,12 @@ public final class SmHotelDownstreamProtocol { /** 慧订:docs/xiayou/shenzhen(静态 + hoteldatepush) */ public static final String HUIDING = "huiding"; - /** 圣诺/奈斯:docs/xiayou/shengnuo(getInfo + plan + price + close + orderChange) */ + /** 圣诺:docs/xiayou/shengnuo(getInfo + plan + price + close + orderChange) */ public static final String SHENGNUO = "shengnuo"; + /** 奈斯:与圣诺同接口形态,独立伙伴配置与入站路径 /third/party/nice/order/* */ + public static final String NICE = "nice"; + /** @deprecated 历史误用名,normalize 后等价 {@link #HUANQI} */ @Deprecated public static final String XIAYOU = "xiayou"; @@ -43,9 +46,12 @@ public final class SmHotelDownstreamProtocol { if (SHENZHEN.equals(p)) { return HUIDING; } - if (NS.equals(p) || "nais".equals(p) || "nice".equals(p)) { + if (NS.equals(p)) { return SHENGNUO; } + if ("nais".equals(p)) { + return NICE; + } return p; } @@ -55,6 +61,7 @@ public final class SmHotelDownstreamProtocol { set.add(HUANQI); set.add(HUIDING); set.add(SHENGNUO); + set.add(NICE); return set; } @@ -70,8 +77,14 @@ public final class SmHotelDownstreamProtocol { return HUIDING.equals(normalize(protocol)); } + /** 圣诺或奈斯(同套推送/验签逻辑) */ public static boolean isShengnuo(String protocol) { - return SHENGNUO.equals(normalize(protocol)); + String p = normalize(protocol); + return SHENGNUO.equals(p) || NICE.equals(p); + } + + public static boolean isNice(String protocol) { + return NICE.equals(normalize(protocol)); } public static String label(String protocol) { @@ -83,7 +96,10 @@ public final class SmHotelDownstreamProtocol { return "慧订"; } if (SHENGNUO.equals(p)) { - return "圣诺/高德"; + return "圣诺"; + } + if (NICE.equals(p)) { + return "奈斯"; } return p; } diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java index 7867f05ed..6dfa8df31 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java @@ -53,7 +53,7 @@ public class SmHotelDownstreamChannelConfigDTO { @Schema(description = "hotel/apartment 每分钟上限,默认 600;≤0 关闭") private Integer rateLimitHotelApartmentPerMin; - @Schema(description = "product/daily 每分钟上限,默认 300;≤0 关闭") + @Schema(description = "product/daily 每分钟上限,默认 150(环启 2026-10-26);≤0 关闭") private Integer rateLimitProductDailyPerMin; @Schema(description = "慧订 hoteldatepush 出站 QPS,默认 5;≤0 关闭") @@ -112,4 +112,7 @@ public class SmHotelDownstreamChannelConfigDTO { @Schema(description = "本地改价/库存/房态后是否自动推本伙伴;大批量补数可临时 false,默认 true(未配则跟进程级)") private Boolean resourceAutoPushEnabled; + + @Schema(description = "全量/日更推送酒店并发;环启建议 ≤3,默认 2;1=串行") + private Integer resourcePushConcurrency; } diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelDailyEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelDailyEntity.java index 2c6774dce..076faf8f5 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelDailyEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelDailyEntity.java @@ -51,6 +51,16 @@ public class SmHotelDailyEntity extends Model { @Schema(description = "可售剩余") private Integer remainStock; + /** 按全局配置动态算的默认卖价(非表字段) */ + @TableField(exist = false) + @Schema(description = "默认卖价=协议价按全局加价") + private BigDecimal defaultSellPrice; + + /** 各启用下游动态卖价 partnerCode → amount(非表字段) */ + @TableField(exist = false) + @Schema(description = "下游渠道卖价映射") + private java.util.Map channelSellPrices; + @TableField(fill = FieldFill.INSERT) private String createBy; diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelFxAgentEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelFxAgentEntity.java index 203fb3c30..10b86eb1e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelFxAgentEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelFxAgentEntity.java @@ -56,6 +56,15 @@ public class SmHotelFxAgentEntity extends Model { private String remark; + @Schema(description = "收款方式 BANK/ALIPAY/WECHAT/OTHER") + private String payMethod; + + @Schema(description = "收款账号") + private String payAccount; + + @Schema(description = "开户名/收款户名") + private String payAccountName; + @TableField(fill = FieldFill.INSERT) private String createBy; diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelFxHotelApplyEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelFxHotelApplyEntity.java new file mode 100644 index 000000000..85eaa9048 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelFxHotelApplyEntity.java @@ -0,0 +1,82 @@ +package com.bsd.pjcloud.supermarket.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import com.bsd.pjcloud.common.core.util.TenantTable; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Data +@TenantTable +@TableName("canteen_hotel_fx_hotel_apply") +@EqualsAndHashCode(callSuper = true) +@Schema(description = "酒店分销-分销员提报酒店") +public class SmHotelFxHotelApplyEntity extends Model { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long tenantId; + + private Long agentId; + + private Long userId; + + private String hotelName; + + private String hotelCode; + + private String district; + + private Long districtAdcode; + + private String address; + + private String contactName; + + private String contactPhone; + + private String remark; + + @Schema(description = "提报扩展 JSON(licenseUrl 等)") + private String hotelJson; + + @Schema(description = "PENDING/APPROVED/REJECTED/CANCELLED") + private String status; + + private Long hotelId; + + @Schema(description = "审核备注;通过时填酒店账号密码等,分销员可见") + private String auditRemark; + + private String auditBy; + + private LocalDateTime auditTime; + + @TableField(fill = FieldFill.INSERT) + private String createBy; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + @TableField(fill = FieldFill.INSERT) + private String delFlag; + + @TableField(exist = false) + private String agentName; + + /** 营业执照图片 URL(来自 hotel_json.licenseUrl,非表字段) */ + @TableField(exist = false) + @Schema(description = "营业执照图片") + private String licenseUrl; +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelMgrPushConfigEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelMgrPushConfigEntity.java index 7a93bfe64..c41c94e6b 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelMgrPushConfigEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelMgrPushConfigEntity.java @@ -33,6 +33,9 @@ public class SmHotelMgrPushConfigEntity extends Model { private String resultMsg; + @Schema(description = "下游请求体快照(失败/部分失败时写入 JSON)") + private String requestJson; + @TableField(fill = FieldFill.INSERT) private String createBy; diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java index 96c9db98e..f1fa05879 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java @@ -58,13 +58,19 @@ public class SmHotelOrderEntity extends Model { private BigDecimal markupPercent; + @Schema(description = "下单时加价固定金额快照") + private BigDecimal markupFixed; + + @Schema(description = "下单计价所用下游 partner,空表示全局/App") + private String pricePartnerCode; + private String contactName; private String contactPhone; private String guestJson; - @Schema(description = "CREATED/PENDING/UPSTREAM_OK/CHECKED_IN/CANCELLED/UPSTREAM_FAIL/PENDING_CONFIRM/NEED_MANUAL") + @Schema(description = "CREATED/PENDING/UPSTREAM_OK/CHECKED_IN/CANCELLED/UPSTREAM_FAIL/BOOK_FAIL/PENDING_CONFIRM/NEED_MANUAL") private String orderStatus; @Schema(description = "上游 bookStatus:P/R/E/X") diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelPriceChannelConfigEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelPriceChannelConfigEntity.java new file mode 100644 index 000000000..1574d362b --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelPriceChannelConfigEntity.java @@ -0,0 +1,57 @@ +package com.bsd.pjcloud.supermarket.entity; + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.activerecord.Model; +import com.bsd.pjcloud.common.core.util.TenantTable; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 酒店下游渠道售价加价:有启用配置则对协议价加价;无配置则推/验协议价原价。 + */ +@Data +@TenantTable +@TableName("canteen_hotel_price_channel_config") +@EqualsAndHashCode(callSuper = true) +@Schema(description = "酒店下游渠道售价加价") +public class SmHotelPriceChannelConfigEntity extends Model { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + private Long tenantId; + + @Schema(description = "下游伙伴编码") + private String partnerCode; + + @Schema(description = "加价百分比:正加价、负折扣") + private BigDecimal markupPercent; + + @Schema(description = "固定加价金额(元)") + private BigDecimal markupFixed; + + @Schema(description = "1启用 0停用") + private String enabled; + + private String remark; + + @TableField(fill = FieldFill.INSERT) + private String createBy; + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updateBy; + + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + + @TableLogic + @TableField(fill = FieldFill.INSERT) + private String delFlag; +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelPriceConfigEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelPriceConfigEntity.java index d6d973d4b..86acc9f8b 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelPriceConfigEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelPriceConfigEntity.java @@ -29,6 +29,9 @@ public class SmHotelPriceConfigEntity extends Model { @Schema(description = "售价调整百分比:正数加价、负数折扣,例15=加价15%,-10=打九折") private BigDecimal markupPercent; + @Schema(description = "固定加价金额(元),可与百分比叠加:sell=cost*(1+p/100)+fixed") + private BigDecimal markupFixed; + private String remark; @TableField(fill = FieldFill.INSERT) diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/SmHotelFxInvitePreviewVO.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/SmHotelFxInvitePreviewVO.java new file mode 100644 index 000000000..c5e41568e --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/vo/SmHotelFxInvitePreviewVO.java @@ -0,0 +1,30 @@ +package com.bsd.pjcloud.supermarket.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 酒店分销邀请码预览(供 UPMS 注册页复用同一邀请码输入框)。 + */ +@Data +@Schema(description = "酒店分销邀请预览") +public class SmHotelFxInvitePreviewVO { + + private String inviteCode; + + private Long parentAgentId; + + private Long parentUserId; + + /** 新用户落入邀请人所属部门 */ + private Long deptId; + + private String parentName; + + private Integer parentLevel; + + /** 新分销员层级:1~3 邀 → 4;4 邀 → 5 */ + private Integer childLevel; + + private String hint; +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java index 2b8023aa7..c8b8a317e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java @@ -67,8 +67,8 @@ public class HotelDownstreamProperties { /** /hotel/sync、/apartment/sync 每分钟上限(环启文档 600) */ private int rateLimitHotelApartmentPerMin = 600; - /** /product/sync、/daily/sync 每分钟上限(环启文档 300) */ - private int rateLimitProductDailyPerMin = 300; + /** /product/sync、/daily/sync 每分钟上限(环启文档 2026-10-26 起 150) */ + private int rateLimitProductDailyPerMin = 150; /** * 慧订 /splider/hoteldatepush 出站 QPS(对方已限流)。 @@ -193,8 +193,8 @@ public class HotelDownstreamProperties { private String resourcePushCron = "0 30 2 * * ?"; /** - * 下游全量推酒店并发(圣诺等 HTTP 出站)。1=串行;建议 4~8。 - * 对方限流时报错再调低。伙伴 YAML 可覆盖。 + * 下游全量推酒店并发。1=串行。 + * 环启文档建议并发≤3(产品/日历写重);默认 2。圣诺若对方宽松可在 YAML 调高。 */ - private int resourcePushConcurrency = 6; + private int resourcePushConcurrency = 2; } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java index 0c969d746..16055a53f 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java @@ -47,16 +47,23 @@ public class SmHotelController { @PreAuthorize("@pms.hasPermission('supermarket_smHotel_view')") public R> page(@ParameterObject Page page, @ParameterObject SmHotelEntity query) { + String nameKw = StrUtil.trim(query.getHotelName()); + boolean nameSearch = StrUtil.isNotBlank(nameKw); var wrapper = Wrappers.lambdaQuery() - .like(StrUtil.isNotBlank(query.getHotelName()), SmHotelEntity::getHotelName, query.getHotelName()) .like(StrUtil.isNotBlank(query.getHotelCode()), SmHotelEntity::getHotelCode, query.getHotelCode()) .like(StrUtil.isNotBlank(query.getGroupCode()), SmHotelEntity::getGroupCode, query.getGroupCode()) .eq(StrUtil.isNotBlank(query.getSourceId()), SmHotelEntity::getSourceId, query.getSourceId()) .eq(StrUtil.isNotBlank(query.getSheetName()), SmHotelEntity::getSheetName, query.getSheetName()) // 归属部门:渠道映射按部门推送时,便于按部门筛酒店核查 - .eq(query.getDeptId() != null, SmHotelEntity::getDeptId, query.getDeptId()) - // 默认按主键倒序:雪花 ID 近似创建时间,可走索引;避免无索引时 create_time 全表 filesort - .orderByDesc(SmHotelEntity::getId); + .eq(query.getDeptId() != null, SmHotelEntity::getDeptId, query.getDeptId()); + // 店名:先走前缀(可用 idx_hotel_name);含匹配去掉 ORDER BY + 不做 COUNT,避免十几万酒店超时 + if (nameSearch) { + wrapper.and(w -> w.likeRight(SmHotelEntity::getHotelName, nameKw) + .or() + .eq(SmHotelEntity::getHotelCode, nameKw) + .or() + .eq(SmHotelEntity::getSHotelId, nameKw)); + } // 外部酒店ID/eid:推圣诺 getInfo 用 sHotelId,空时回落 hotelCode if (StrUtil.isNotBlank(query.getSHotelId())) { String eid = query.getSHotelId().trim(); @@ -74,7 +81,40 @@ public class SmHotelController { .ne(SmHotelEntity::getUpstreamStatus, "OFFLINE")); } hotelService.applySaleableFilter(wrapper, query.getSaleable()); - return R.ok(hotelService.pageScope(page, wrapper)); + if (!nameSearch) { + // 默认按主键倒序:雪花 ID 近似创建时间,可走索引 + wrapper.orderByDesc(SmHotelEntity::getId); + return R.ok(hotelService.pageScope(page, wrapper)); + } + // 名称搜索:先前缀(可排序);无结果再含匹配且不做 COUNT / 不 ORDER BY + wrapper.orderByDesc(SmHotelEntity::getId); + Page prefixPage = hotelService.pageScopeWithoutCount(page, wrapper); + if (page.getCurrent() > 1 || (prefixPage.getRecords() != null && !prefixPage.getRecords().isEmpty())) { + return R.ok(prefixPage); + } + var contains = Wrappers.lambdaQuery() + .like(SmHotelEntity::getHotelName, nameKw) + .like(StrUtil.isNotBlank(query.getHotelCode()), SmHotelEntity::getHotelCode, query.getHotelCode()) + .like(StrUtil.isNotBlank(query.getGroupCode()), SmHotelEntity::getGroupCode, query.getGroupCode()) + .eq(StrUtil.isNotBlank(query.getSourceId()), SmHotelEntity::getSourceId, query.getSourceId()) + .eq(StrUtil.isNotBlank(query.getSheetName()), SmHotelEntity::getSheetName, query.getSheetName()) + .eq(query.getDeptId() != null, SmHotelEntity::getDeptId, query.getDeptId()); + if (StrUtil.isNotBlank(query.getSHotelId())) { + String eid = query.getSHotelId().trim(); + contains.and(w -> w.eq(SmHotelEntity::getSHotelId, eid).or().eq(SmHotelEntity::getHotelCode, eid)); + } + HotelAreaAdcodeUtil.applyHotelListFilter(contains, query.getDistrictAdcode(), query.getDistrict()); + if ("OFFLINE".equalsIgnoreCase(query.getUpstreamStatus())) { + contains.eq(SmHotelEntity::getUpstreamStatus, "OFFLINE"); + } + else if ("ACTIVE".equalsIgnoreCase(query.getUpstreamStatus())) { + contains.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus) + .or() + .ne(SmHotelEntity::getUpstreamStatus, "OFFLINE")); + } + hotelService.applySaleableFilter(contains, query.getSaleable()); + // 含匹配:禁止 ORDER BY,让 MySQL 凑够 LIMIT 即可停,避免全表 filesort + return R.ok(hotelService.pageScopeWithoutCount(page, contains)); } /** 上传文件会立刻 transferTo 临时目录,勿加 @SysLog */ diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelFxController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelFxController.java index 35fd669b2..b2d40a7a3 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelFxController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelFxController.java @@ -1,5 +1,6 @@ package com.bsd.pjcloud.supermarket.controller; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.log.annotation.SysLog; @@ -7,6 +8,7 @@ import com.bsd.pjcloud.supermarket.entity.SmHotelFxAgentEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelFxCommissionEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelFxConfigEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelFxExitEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelFxHotelApplyEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelFxHotelBindEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelFxL1ApplyEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelFxWithdrawEntity; @@ -14,6 +16,7 @@ import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService; import com.bsd.pjcloud.supermarket.service.SmHotelFxCommissionService; import com.bsd.pjcloud.supermarket.service.SmHotelFxConfigService; import com.bsd.pjcloud.supermarket.service.SmHotelFxExitService; +import com.bsd.pjcloud.supermarket.service.SmHotelFxHotelApplyService; import com.bsd.pjcloud.supermarket.service.SmHotelFxL1ApplyService; import com.bsd.pjcloud.supermarket.service.SmHotelFxWithdrawService; import io.swagger.v3.oas.annotations.Operation; @@ -22,6 +25,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.util.Map; /** @@ -45,6 +49,8 @@ public class SmHotelFxController { private final SmHotelFxExitService exitService; + private final SmHotelFxHotelApplyService hotelApplyService; + @Operation(summary = "分销配置") @GetMapping("/config") @PreAuthorize("@pms.hasPermission('supermarket_smHotelFx_view')") @@ -118,6 +124,28 @@ public class SmHotelFxController { return R.ok(agentService.pageBinds(page, agentId, hotelId)); } + @Operation(summary = "按酒店ID批量查已绑定分销员(审核选店)") + @GetMapping("/bind/byHotels") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelFxAgent_view') || @pms.hasPermission('supermarket_smHotelFxHotelApply_view') || @pms.hasPermission('supermarket_smHotelFxHotelApply_audit')") + public R> bindsByHotels(@RequestParam String hotelIds) { + java.util.List ids = new java.util.ArrayList<>(); + if (cn.hutool.core.util.StrUtil.isNotBlank(hotelIds)) { + for (String p : hotelIds.split(",")) { + String t = p.trim(); + if (cn.hutool.core.util.StrUtil.isBlank(t)) { + continue; + } + try { + ids.add(Long.parseLong(t)); + } + catch (NumberFormatException ignored) { + // skip bad token + } + } + } + return R.ok(agentService.listBoundByHotelIds(ids)); + } + @Operation(summary = "佣金明细分页(管理端)") @GetMapping("/commission/page") @PreAuthorize("@pms.hasPermission('supermarket_smHotelFxCommission_view')") @@ -195,7 +223,11 @@ public class SmHotelFxController { @Operation(summary = "我的佣金汇总(App)") @GetMapping("/app/mySummary") public R> mySummary() { - return commissionService.mySummary(); + R> r = commissionService.mySummary(); + if (r != null && r.isOk() && r.getData() != null) { + r.getData().put("pendingApplyCount", hotelApplyService.countPendingMine()); + } + return r; } @Operation(summary = "我的佣金明细(App)") @@ -205,6 +237,22 @@ public class SmHotelFxController { return R.ok(commissionService.pageMine(page, status)); } + @Operation(summary = "下线绩效汇总(App:直接下级佣金归本人,上级只读)") + @GetMapping("/app/downlinePerf") + public R> downlinePerf() { + return commissionService.downlinePerf(); + } + + @SysLog("分销模拟下单") + @Operation(summary = "模拟下单记佣(管理端联调:酒店须已绑定分销员)") + @PostMapping("/mockOrder") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelFx_edit')") + public R> mockOrder(@RequestParam Long hotelId, + @RequestParam(required = false) BigDecimal costAmount, + @RequestParam(required = false, defaultValue = "false") Boolean lockNow) { + return commissionService.mockOrderForBoundHotel(hotelId, costAmount, lockNow); + } + @Operation(summary = "申请一级(App)") @PostMapping("/app/l1/apply") public R appL1Apply(@RequestParam String cityAdcode, @@ -238,4 +286,90 @@ public class SmHotelFxController { public R appExitApply(@RequestParam(required = false) String reason) { return exitService.applyMine(reason); } + + @Operation(summary = "酒店提报分页(管理端)") + @GetMapping("/hotelApply/page") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelFxHotelApply_view')") + public R> hotelApplyPage(Page page, + SmHotelFxHotelApplyEntity query) { + return R.ok(hotelApplyService.pageAdmin(page, query)); + } + + @SysLog("审核分销酒店提报") + @Operation(summary = "审核酒店提报(通过须选已完善酒店 + 备注账号密码)") + @PostMapping("/hotelApply/audit") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelFxHotelApply_audit')") + public R hotelApplyAudit(@RequestParam Long id, @RequestParam boolean approved, + @RequestParam(required = false) String auditRemark, + @RequestParam(required = false) Long hotelId) { + return hotelApplyService.audit(id, approved, auditRemark, hotelId); + } + + @Operation(summary = "我的酒店提报(App)") + @GetMapping("/app/hotelApply/page") + public R> appHotelApplyPage(Page page, + @RequestParam(required = false) String status) { + return R.ok(hotelApplyService.pageMine(page, status)); + } + + @Operation(summary = "我的提报详情(App,编辑回填)") + @GetMapping("/app/hotelApply/{id}") + public R appHotelApplyDetail(@PathVariable Long id) { + return hotelApplyService.getMine(id); + } + + @Operation(summary = "新增/重提酒店(App:名称+地址+营业执照;传 id 可编辑待审/驳回)") + @PostMapping("/app/hotelApply") + public R appHotelApply(@RequestBody Map body) { + SmHotelFxHotelApplyEntity row = new SmHotelFxHotelApplyEntity(); + if (body != null) { + Object idObj = body.get("id"); + if (idObj != null && StrUtil.isNotBlank(String.valueOf(idObj))) { + try { + row.setId(Long.valueOf(String.valueOf(idObj))); + } + catch (Exception ignored) { + // ignore bad id + } + } + Object json = body.get("hotelJson"); + if (json instanceof String) { + row.setHotelJson((String) json); + } + else if (json != null) { + row.setHotelJson(cn.hutool.json.JSONUtil.toJsonStr(json)); + } + else { + Map copy = new java.util.LinkedHashMap<>(body); + copy.remove("id"); + row.setHotelJson(cn.hutool.json.JSONUtil.toJsonStr(copy)); + } + Object name = body.get("hotelName"); + if (name != null) { + row.setHotelName(String.valueOf(name)); + } + Object address = body.get("address"); + if (address != null) { + row.setAddress(String.valueOf(address)); + } + Object license = body.get("licenseUrl"); + if (license == null) { + license = body.get("businessLicense"); + } + if (license != null) { + row.setLicenseUrl(String.valueOf(license)); + } + Object remark = body.get("remark"); + if (remark != null) { + row.setRemark(String.valueOf(remark)); + } + } + return hotelApplyService.applyMine(row); + } + + @Operation(summary = "我的绑定酒店(App)") + @GetMapping("/app/myHotels") + public R> appMyHotels(Page page) { + return R.ok(agentService.pageMineHotels(page)); + } } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelFxInviteInnerController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelFxInviteInnerController.java new file mode 100644 index 000000000..4ea61a308 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelFxInviteInnerController.java @@ -0,0 +1,42 @@ +package com.bsd.pjcloud.supermarket.controller; + +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.security.annotation.Inner; +import com.bsd.pjcloud.supermarket.service.SmHotelFxAgentService; +import com.bsd.pjcloud.supermarket.vo.SmHotelFxInvitePreviewVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 酒店分销邀请内部接口:供 UPMS 邀请注册识别分销邀请码。 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/inner/smHotelFx/invite") +@Tag(name = "酒店分销邀请-内部") +public class SmHotelFxInviteInnerController { + + private final SmHotelFxAgentService agentService; + + @Inner + @Operation(summary = "分销邀请码预览") + @GetMapping("/preview") + public R preview(@RequestParam String inviteCode) { + return agentService.previewInvite(inviteCode); + } + + @Inner + @Operation(summary = "注册后挂靠分销员") + @PostMapping("/bindAgent") + public R bindAgent(@RequestParam String inviteCode, @RequestParam Long userId, + @RequestParam(required = false) String username, @RequestParam(required = false) String realName, + @RequestParam(required = false) String mobile) { + return agentService.createAgentByInvite(inviteCode, userId, username, realName, mobile); + } +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java index 3eec03f6b..0d4c3a59e 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java @@ -6,6 +6,7 @@ import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO; import com.bsd.pjcloud.supermarket.dto.SmHotelSyncFailExcel; import com.bsd.pjcloud.supermarket.dto.SmHotelSyncFailLineVO; import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity; +import com.bsd.pjcloud.supermarket.entity.SmHotelPriceChannelConfigEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelPriceConfigEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity; @@ -29,6 +30,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -69,13 +71,43 @@ public class SmHotelMidController { } @SysLog("全局同步酒店售价(仅第三方来源,按配置重算产品/日历,不动订单与本地酒店)") - @Operation(summary = "按当前配置重算第三方酒店产品与日历卖价(排除数据来源=本地;不动订单)") + @Operation(summary = "按当前全局配置重算第三方酒店默认卖价(排除本地;不动订单与下游渠道价)") @PostMapping("/priceConfig/resyncSell") @PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_edit')") public R> resyncSellPrices() { return priceConfigService.resyncAllSellPrices(); } + @Operation(summary = "下游渠道加价配置列表") + @GetMapping("/priceConfig/channels") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_view')") + public R> listPriceChannels() { + return priceConfigService.listChannelConfigs(); + } + + @SysLog("保存酒店下游渠道加价") + @Operation(summary = "保存下游渠道加价(有配置则对该伙伴按公式加价;删除后推原价)") + @PutMapping("/priceConfig/channels") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_edit')") + public R savePriceChannel(@RequestBody SmHotelPriceChannelConfigEntity entity) { + return priceConfigService.saveChannelConfig(entity); + } + + @SysLog("删除酒店下游渠道加价") + @Operation(summary = "删除下游渠道加价配置(之后该伙伴推/验协议价原价)") + @DeleteMapping("/priceConfig/channels/{id}") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_edit')") + public R deletePriceChannel(@PathVariable Long id) { + return priceConfigService.deleteChannelConfig(id); + } + + @Operation(summary = "预览协议价在全局/各下游下的卖价") + @GetMapping("/priceConfig/preview") + @PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_view')") + public R> previewPrice(@RequestParam(required = false) BigDecimal basePrice) { + return priceConfigService.previewSellPrices(basePrice); + } + @Operation(summary = "酒店详情(含已拉取房型/产品/日历预览)") @GetMapping("/detail/{id}") @PreAuthorize("@pms.hasPermission('supermarket_smHotel_view')") diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelNiceOrderController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelNiceOrderController.java new file mode 100644 index 000000000..7774f0b23 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelNiceOrderController.java @@ -0,0 +1,90 @@ +package com.bsd.pjcloud.supermarket.controller; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.bsd.pjcloud.common.security.annotation.Inner; +import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamInboundAccessLog; +import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamInboundAuth; +import com.bsd.pjcloud.supermarket.service.SmHotelShengnuoOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiFunction; + +/** + * 奈斯订单入站(与圣诺同协议、同验签;独立回调路径,伙伴 appid/secret 在「渠道对接配置」各自维护): + *
+ * POST /third/party/nice/order/createOrder
+ * POST /third/party/nice/order/orderCancel
+ * POST /third/party/nice/order/orderConfirmTimeOut
+ * 
+ * 网关完整路径示例:https://域名/api/supermarket/third/party/nice/order/createOrder + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/third/party/nice/order") +@Tag(name = "奈斯酒店订单对接") +public class SmHotelNiceOrderController { + + private final DownstreamInboundAuth inboundAuth; + + private final SmHotelShengnuoOrderService shengnuoOrderService; + + private final DownstreamInboundAccessLog inboundAccessLog; + + @Inner(value = false) + @Operation(summary = "奈斯推送下单") + @PostMapping("/createOrder") + public Map createOrder(HttpServletRequest request, @RequestBody String rawBody) { + return handle(request, "inbound_create", rawBody, shengnuoOrderService::createOrder); + } + + @Inner(value = false) + @Operation(summary = "奈斯推送取消") + @PostMapping("/orderCancel") + public Map orderCancel(HttpServletRequest request, @RequestBody String rawBody) { + return handle(request, "inbound_cancel", rawBody, shengnuoOrderService::orderCancel); + } + + @Inner(value = false) + @Operation(summary = "奈斯确认超时提醒") + @PostMapping("/orderConfirmTimeOut") + public Map orderConfirmTimeOut(HttpServletRequest request, @RequestBody String rawBody) { + return handle(request, "inbound_timeout", rawBody, shengnuoOrderService::orderConfirmTimeOut); + } + + private Map handle(HttpServletRequest request, String action, String rawBody, + BiFunction> fn) { + long t0 = System.currentTimeMillis(); + String partner = null; + Map resp; + try { + partner = inboundAuth.verifyForShengnuo(rawBody); + JSONObject body = JSONUtil.parseObj(rawBody); + resp = fn.apply(partner, body); + } + catch (Exception e) { + log.warn("|奈斯订单|{}|失败|{}", action, e.getMessage()); + resp = new LinkedHashMap<>(6); + resp.put("code", 1); + resp.put("success", false); + resp.put("msg", StrUtil.blankToDefault(e.getMessage(), "处理失败")); + resp.put("status", "error"); + resp.put("data", Map.of()); + } + inboundAccessLog.record(partner, action, request.getRequestURI(), rawBody, resp, + System.currentTimeMillis() - t0); + return resp; + } +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelShengnuoOrderController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelShengnuoOrderController.java index a6c57a2ff..9cfe575e5 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelShengnuoOrderController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelShengnuoOrderController.java @@ -22,12 +22,13 @@ import java.util.Map; import java.util.function.BiFunction; /** - * 圣诺/奈斯订单入站(供应商回调地址,给对方配置): + * 圣诺订单入站(供应商回调地址,给对方配置): *
  * POST /third/party/shengnuo/order/createOrder
  * POST /third/party/shengnuo/order/orderCancel
  * POST /third/party/shengnuo/order/orderConfirmTimeOut
  * 
+ * 奈斯同协议另见 {@link SmHotelNiceOrderController}(/third/party/nice/order/*)。 * 网关完整路径示例:https://域名/api/supermarket/third/party/shengnuo/order/createOrder */ @Slf4j diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamHotelAdapterRegistry.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamHotelAdapterRegistry.java index 882ee6182..95426ec37 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamHotelAdapterRegistry.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamHotelAdapterRegistry.java @@ -28,6 +28,11 @@ public class DownstreamHotelAdapterRegistry { byProtocol.put(key, adapter); } } + // 奈斯与圣诺同接口:复用圣诺适配器,避免再声明继承 Bean 造成按类型注入冲突 + DownstreamHotelAdapter shengnuo = byProtocol.get(SmHotelDownstreamProtocol.SHENGNUO); + if (shengnuo != null && !byProtocol.containsKey(SmHotelDownstreamProtocol.NICE)) { + byProtocol.put(SmHotelDownstreamProtocol.NICE, shengnuo); + } } public DownstreamHotelAdapter require(String protocol) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamInboundAuth.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamInboundAuth.java index 168b03210..271460f88 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamInboundAuth.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/DownstreamInboundAuth.java @@ -441,7 +441,7 @@ public class DownstreamInboundAuth { } } if (StrUtil.isBlank(partner) || StrUtil.isBlank(secret)) { - throw new CheckedException("appid 未匹配到启用的圣诺下游配置"); + throw new CheckedException("appid 未匹配到启用的圣诺或奈斯下游配置"); } if (!ShengnuoSignHelper.matches(appId, secret, template, sign)) { throw new CheckedException("签名错误"); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/huanqi/HuanqiDownstreamHotelAdapter.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/huanqi/HuanqiDownstreamHotelAdapter.java index 309671875..08b3afb7b 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/huanqi/HuanqiDownstreamHotelAdapter.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/huanqi/HuanqiDownstreamHotelAdapter.java @@ -187,14 +187,17 @@ public class HuanqiDownstreamHotelAdapter implements DownstreamHotelAdapter { } } - /** 按 appKey + 接口组做分钟窗限流(多实例共享 Redis) */ + /** + * 按 appKey + 接口组做分钟窗限流(多实例共享 Redis)。 + * 超限时必须 decrement 退回占用并等待,禁止「等超时仍继续」——否则会把对方打到「访问过于频繁」。 + */ private void acquireRatePermit(HotelDownstreamProperties props, String path) { int limit = resolveRateLimit(props, path); if (limit <= 0 || StrUtil.isBlank(props.getAppKey())) { return; } String group = rateGroup(path); - for (int i = 0; i < 60; i++) { + for (int i = 0; i < 120; i++) { long minute = System.currentTimeMillis() / 60_000L; String key = RATE_KEY + props.getAppKey() + ":" + group + ":" + minute; Long n = redisTemplate.opsForValue().increment(key); @@ -204,10 +207,17 @@ public class HuanqiDownstreamHotelAdapter implements DownstreamHotelAdapter { if (n == null || n <= limit) { return; } - long wait = 60_000L - (System.currentTimeMillis() % 60_000L) + 20L; - sleepQuiet(Math.min(wait, 5000L)); + // 超限:退回本次数,避免循环里反复 increment 把窗口计数打爆 + try { + redisTemplate.opsForValue().decrement(key); + } + catch (Exception ignored) { + // ignore + } + long wait = 60_000L - (System.currentTimeMillis() % 60_000L) + 80L; + sleepQuiet(Math.min(Math.max(wait, 200L), 3000L)); } - log.warn("|{}|限流等待超时仍继续|appKey={}|path={}|limit={}|", TAG, props.getAppKey(), path, limit); + throw new CheckedException("下游限流等待超时(" + group + "≤" + limit + "/min),请降低推送并发或错峰再试"); } private static int resolveRateLimit(HotelDownstreamProperties props, String path) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/shengnuo/ShengnuoDownstreamHotelAdapter.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/shengnuo/ShengnuoDownstreamHotelAdapter.java index c18da6610..c58e80c0b 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/shengnuo/ShengnuoDownstreamHotelAdapter.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/downstream/shengnuo/ShengnuoDownstreamHotelAdapter.java @@ -20,7 +20,8 @@ import java.util.LinkedHashMap; import java.util.Map; /** - * 圣诺/奈斯下游:getInfo + pushHotelPlan + pushHotelPrice + pushHotelClose + orderChange。 + * 圣诺下游:getInfo + pushHotelPlan + pushHotelPrice + pushHotelClose + orderChange。 + * 奈斯协议复用本适配器(见 {@link com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelAdapterRegistry})。 * 签名 md5('ns'+appid+secretKey+template);成功 code=0。 */ @Slf4j diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDailyMapper.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDailyMapper.java index fd0ad8aa5..e01acb315 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDailyMapper.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDailyMapper.java @@ -30,6 +30,10 @@ public interface SmHotelDailyMapper extends PjcloudBaseMapper list); /** - * 按底价重算日历卖价:sell = ROUND(cost * rate, 2)。仅第三方来源酒店;本地 sheet_name=local 不参与。 + * 按底价重算日历默认卖价:sell = ROUND(cost * rate + fixed, 2)。仅第三方来源;本地不参与。 */ @Update(""" UPDATE canteen_hotel_daily d INNER JOIN canteen_hotel h ON h.id = d.hotel_id AND h.del_flag = '0' - SET d.sell_price = ROUND(d.cost_price * #{rate}, 2), + SET d.sell_price = GREATEST(0.01, ROUND(d.cost_price * #{rate} + #{fixed}, 2)), d.update_time = NOW() WHERE d.del_flag = '0' AND d.cost_price IS NOT NULL AND (h.sheet_name IS NULL OR LOWER(TRIM(h.sheet_name)) <> 'local') """) - int resyncSellPriceByCost(@Param("rate") BigDecimal rate); + int resyncSellPriceByCost(@Param("rate") BigDecimal rate, @Param("fixed") BigDecimal fixed); @Select("""