Merge branch 'master' of https://gitea.zianxin.com/zianxin/biz-api-cloud-zax
This commit is contained in:
commit
413394014e
21 files changed
+1303
-72
No files matched your search
@@ -66,6 +66,9 @@ node_modules
|
||||
### 充值导入测试脚本(含 Excel 原始数据与本地配置) ###
|
||||
scripts/recharge-import/
|
||||
|
||||
#文档#
|
||||
*.md
|
||||
|
||||
### AI 辅助工具 ###
|
||||
.video_agent
|
||||
.mimosa
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.bsd.pjcloud.supermarket.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
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.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("canteen_hotel_avail_sub")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "酒店有房通知订阅")
|
||||
public class SmHotelAvailSubEntity extends Model<SmHotelAvailSubEntity> {
|
||||
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
public static final String STATUS_NOTIFIED = "NOTIFIED";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
/** 指定入住离店区间,整段可订才通知 */
|
||||
public static final String WATCH_RANGE = "RANGE";
|
||||
|
||||
/** 不限日期:近窗内任意一晚可订即通知 */
|
||||
public static final String WATCH_ANY = "ANY";
|
||||
|
||||
/** ANY 模式落库占位入住日(不参与业务判定) */
|
||||
public static final LocalDate ANY_SENTINEL_IN = LocalDate.of(1900, 1, 1);
|
||||
|
||||
/** ANY 模式落库占位离店日(不参与业务判定) */
|
||||
public static final LocalDate ANY_SENTINEL_OUT = LocalDate.of(1900, 1, 2);
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private LocalDate checkIn;
|
||||
|
||||
private LocalDate checkOut;
|
||||
|
||||
private Integer rooms;
|
||||
|
||||
@Schema(description = "RANGE指定日期 / ANY不限日期")
|
||||
private String watchMode;
|
||||
|
||||
@Schema(description = "ACTIVE/NOTIFIED/CANCELLED")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "推送幂等版本")
|
||||
private Integer notifyVersion;
|
||||
|
||||
private String hotelName;
|
||||
|
||||
private String productName;
|
||||
|
||||
private LocalDateTime notifiedTime;
|
||||
|
||||
@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;
|
||||
}
|
||||
+9
@@ -163,4 +163,13 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "酒店数据来源")
|
||||
private String hotelDataSource;
|
||||
|
||||
/** 列表展示:酒店归属部门(对应商户,结算用;非表字段,来自 canteen_hotel.dept_id) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "酒店归属部门ID")
|
||||
private Long deptId;
|
||||
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "酒店归属部门名称")
|
||||
private String deptName;
|
||||
}
|
||||
+10
@@ -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
|
||||
@@ -37,6 +38,15 @@ public class SmHotelRoomEntity extends Model<SmHotelRoomEntity> {
|
||||
|
||||
private Integer roomNumber;
|
||||
|
||||
@Schema(description = "楼层(如 3-5层)")
|
||||
private String floorInfo;
|
||||
|
||||
@Schema(description = "面积(㎡)")
|
||||
private BigDecimal areaSqm;
|
||||
|
||||
@Schema(description = "窗型:有窗/无窗/部分有窗/其他")
|
||||
private String windowType;
|
||||
|
||||
private String rawJson;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
|
||||
+3
@@ -84,6 +84,9 @@ public class SmHotelOrderDetailVo {
|
||||
private BigDecimal latitude;
|
||||
|
||||
private Long deptId;
|
||||
|
||||
@Schema(description = "酒店归属部门名称(商户)")
|
||||
private String deptName;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
+146
-21
@@ -11,6 +11,7 @@ import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.security.service.PjcloudUser;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelAvailSubEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
|
||||
@@ -21,6 +22,7 @@ import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelProductMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelRoomMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelUserSearchPrefMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelService;
|
||||
@@ -41,7 +43,6 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -91,7 +92,7 @@ public class SmHotelAppController {
|
||||
private static final String CANDIDATE_CACHE_OVERFLOW = "OVERFLOW";
|
||||
|
||||
/** 列表页结果短缓存(同条件重复进入/下拉) */
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v13:";
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v14:";
|
||||
|
||||
private static final long PAGE_CACHE_SECONDS = 45L;
|
||||
|
||||
@@ -121,6 +122,8 @@ public class SmHotelAppController {
|
||||
|
||||
private final SmHotelProductMapper productMapper;
|
||||
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Operation(summary = "列表筛选项(区域/星级/设施)")
|
||||
@@ -425,16 +428,32 @@ public class SmHotelAppController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按酒店名搜索:
|
||||
* 1) 前缀匹配(idx_hotel_name + ORDER BY,快);
|
||||
* 2) 无结果再含匹配——SQL 不做 ORDER BY,只 LIMIT,再内存排序(避免超时变空列表)。
|
||||
* 有城市时尽量落到 district_adcode 区间。
|
||||
* 按酒店名搜索(只打 canteen_hotel,几万级):
|
||||
* 1) 先全国店名查出候选(前缀 → 含匹配),不带区划、不带日历,避免「有城市但酒店未落 adcode」直接变空;
|
||||
* 2) 有城市时内存把本市排前面;本市一个都没有则仍返回全国店名命中(总比超时/无语强);
|
||||
* 3) 日历最低价仍只查本页 id。
|
||||
*/
|
||||
private Page<SmHotelEntity> pageByHotelNameKeyword(Page<SmHotelEntity> page, String keyword, String city,
|
||||
String district, Long districtAdcode, List<Integer> starList) {
|
||||
long size = Math.max(1L, Math.min(page.getSize(), 50L));
|
||||
long current = Math.max(1L, page.getCurrent());
|
||||
int fetch = (int) Math.min(KEYWORD_CONTAINS_FETCH, Math.max(size + 1, size * current + 1));
|
||||
int fetch = KEYWORD_CONTAINS_FETCH;
|
||||
long tName = System.currentTimeMillis();
|
||||
List<SmHotelEntity> rows = hotelMapper.selectAppByNamePrefix(keyword, fetch);
|
||||
String hit = "prefix";
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
rows = hotelMapper.selectAppByNameContains(keyword, fetch);
|
||||
hit = "contains";
|
||||
}
|
||||
if (rows == null) {
|
||||
rows = List.of();
|
||||
}
|
||||
Set<Integer> starSet = starList == null || starList.isEmpty() ? Set.of() : new HashSet<>(starList);
|
||||
if (!starSet.isEmpty()) {
|
||||
rows = rows.stream()
|
||||
.filter(h -> h.getStarLevel() != null && starSet.contains(h.getStarLevel()))
|
||||
.toList();
|
||||
}
|
||||
Long adFrom = null;
|
||||
Long adTo = null;
|
||||
Long adcode = districtAdcode;
|
||||
@@ -449,20 +468,7 @@ public class SmHotelAppController {
|
||||
adFrom = range[0];
|
||||
adTo = range[1];
|
||||
}
|
||||
Collection<Integer> stars = starList == null || starList.isEmpty() ? null : starList;
|
||||
List<SmHotelEntity> rows = hotelMapper.selectAppByNamePrefix(keyword, adFrom, adTo, stars, fetch);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
// 含匹配绝不带 ORDER BY;有城市时区间小,全国则最多捞 KEYWORD_CONTAINS_FETCH 条后内存排
|
||||
rows = hotelMapper.selectAppByNameContains(keyword, adFrom, adTo, stars, fetch);
|
||||
}
|
||||
if (rows == null) {
|
||||
rows = List.of();
|
||||
}
|
||||
List<SmHotelEntity> sorted = new ArrayList<>(rows);
|
||||
sorted.sort(Comparator
|
||||
.comparing((SmHotelEntity h) -> h.getStarLevel() == null ? 0 : h.getStarLevel())
|
||||
.reversed()
|
||||
.thenComparing((SmHotelEntity h) -> h.getId() == null ? 0L : h.getId(), Comparator.reverseOrder()));
|
||||
List<SmHotelEntity> sorted = rankKeywordHits(rows, adFrom, adTo);
|
||||
int from = (int) Math.min((current - 1) * size, sorted.size());
|
||||
int to = (int) Math.min(from + size, sorted.size());
|
||||
List<SmHotelEntity> slice = from >= sorted.size() ? List.of() : sorted.subList(from, to);
|
||||
@@ -470,6 +476,46 @@ public class SmHotelAppController {
|
||||
long total = (current - 1) * size + slice.size() + (hasMore ? 1 : 0);
|
||||
Page<SmHotelEntity> out = new Page<>(current, size, total);
|
||||
out.setRecords(new ArrayList<>(slice));
|
||||
log.info("|酒店App|店名搜索|kw={}|hit={}|cand={}|page={}|adcode={}|costMs={}|", keyword, hit, sorted.size(),
|
||||
slice.size(), adcode, System.currentTimeMillis() - tName);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 本市(adcode 命中)优先,再按星级、id;本市没有则保留全国店名命中 */
|
||||
private static List<SmHotelEntity> rankKeywordHits(List<SmHotelEntity> rows, Long adFrom, Long adTo) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<SmHotelEntity> inCity = new ArrayList<>();
|
||||
List<SmHotelEntity> other = new ArrayList<>();
|
||||
for (SmHotelEntity h : rows) {
|
||||
if (h == null) {
|
||||
continue;
|
||||
}
|
||||
if (adFrom != null && adTo != null && h.getDistrictAdcode() != null
|
||||
&& h.getDistrictAdcode() >= adFrom && h.getDistrictAdcode() < adTo) {
|
||||
inCity.add(h);
|
||||
}
|
||||
else {
|
||||
other.add(h);
|
||||
}
|
||||
}
|
||||
Comparator<SmHotelEntity> byStar = Comparator
|
||||
.comparing((SmHotelEntity h) -> h.getStarLevel() == null ? 0 : h.getStarLevel())
|
||||
.reversed()
|
||||
.thenComparing((SmHotelEntity h) -> h.getId() == null ? 0L : h.getId(), Comparator.reverseOrder());
|
||||
inCity.sort(byStar);
|
||||
other.sort(byStar);
|
||||
List<SmHotelEntity> out = new ArrayList<>(inCity.size() + other.size());
|
||||
out.addAll(inCity);
|
||||
// 本市一个都没有时,用全国店名结果,避免「库里有如家、界面无语」
|
||||
if (inCity.isEmpty()) {
|
||||
out.addAll(other);
|
||||
}
|
||||
else {
|
||||
// 本市有结果时仍可附带少量市外,便于翻页;首页优先本市
|
||||
out.addAll(other);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -837,6 +883,73 @@ public class SmHotelAppController {
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "订阅有房通知")
|
||||
@PostMapping("/availability-subscribe")
|
||||
public R<SmHotelAvailSubEntity> availabilitySubscribe(@RequestBody Map<String, Object> body) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
if (body == null) {
|
||||
body = Map.of();
|
||||
}
|
||||
Long hotelId = parseLong(body.get("hotelId"));
|
||||
String productCode = str(body.get("productCode"));
|
||||
LocalDate checkIn = parseDay(str(body.get("checkIn")));
|
||||
LocalDate checkOut = parseDay(str(body.get("checkOut")));
|
||||
Integer rooms = clampInt(body.get("rooms"), 1, 20, 1);
|
||||
String watchMode = str(body.get("watchMode"));
|
||||
return availabilityNotifyService.subscribe(user.getId(), hotelId, productCode, checkIn, checkOut, rooms,
|
||||
watchMode);
|
||||
}
|
||||
|
||||
@Operation(summary = "取消有房通知订阅")
|
||||
@PostMapping("/availability-unsubscribe")
|
||||
public R<Boolean> availabilityUnsubscribe(@RequestBody Map<String, Object> body) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
if (body == null) {
|
||||
body = Map.of();
|
||||
}
|
||||
Long id = parseLong(body.get("id"));
|
||||
Long hotelId = parseLong(body.get("hotelId"));
|
||||
String productCode = str(body.get("productCode"));
|
||||
LocalDate checkIn = parseDay(str(body.get("checkIn")));
|
||||
LocalDate checkOut = parseDay(str(body.get("checkOut")));
|
||||
Integer rooms = clampInt(body.get("rooms"), 1, 20, 1);
|
||||
String watchMode = str(body.get("watchMode"));
|
||||
return availabilityNotifyService.unsubscribe(user.getId(), id, hotelId, productCode, checkIn, checkOut, rooms,
|
||||
watchMode);
|
||||
}
|
||||
|
||||
@Operation(summary = "我的有房通知订阅")
|
||||
@GetMapping("/availability-subscriptions")
|
||||
public R<Page<SmHotelAvailSubEntity>> availabilitySubscriptions(@ParameterObject Page<?> page) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
return R.ok(availabilityNotifyService.pageMine(page, user.getId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "有房通知订阅状态")
|
||||
@GetMapping("/availability-subscribe/status")
|
||||
public R<Map<String, Object>> availabilitySubscribeStatus(
|
||||
@RequestParam Long hotelId,
|
||||
@RequestParam String productCode,
|
||||
@RequestParam String checkIn,
|
||||
@RequestParam String checkOut,
|
||||
@RequestParam(required = false) Integer rooms) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
return R.ok(availabilityNotifyService.status(user.getId(), hotelId, productCode,
|
||||
parseDay(checkIn), parseDay(checkOut), rooms));
|
||||
}
|
||||
|
||||
@Operation(summary = "酒店日历分页")
|
||||
@GetMapping("/detail/{id}/daily")
|
||||
public R<Page<SmHotelDailyEntity>> daily(@PathVariable Long id,
|
||||
@@ -1368,4 +1481,16 @@ public class SmHotelAppController {
|
||||
private static String str(Object o) {
|
||||
return o == null ? null : String.valueOf(o);
|
||||
}
|
||||
|
||||
private static Long parseLong(Object raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(raw).trim());
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -7,6 +7,7 @@ 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.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
|
||||
@@ -42,6 +43,8 @@ public class SmHotelMidController {
|
||||
|
||||
private final SmHotelChannelDeptService channelDeptService;
|
||||
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
@Operation(summary = "查询全局售价调整配置(加价/折扣)")
|
||||
@GetMapping("/priceConfig")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_view')")
|
||||
@@ -300,6 +303,14 @@ public class SmHotelMidController {
|
||||
return midService.batchLocalDaily(hotelId, req);
|
||||
}
|
||||
|
||||
@SysLog("手动扫描有房通知订阅")
|
||||
@Operation(summary = "立即扫描该酒店 ACTIVE 有房订阅并尝试推送(联调/补推)")
|
||||
@PostMapping("/local/{hotelId}/avail-notify/scan")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Map<String, Object>> scanAvailNotify(@PathVariable Long hotelId) {
|
||||
return R.ok(availabilityNotifyService.scanNow(hotelId));
|
||||
}
|
||||
|
||||
@SysLog("本地酒店删除日历")
|
||||
@DeleteMapping("/local/{hotelId}/daily/{dailyId}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.bsd.pjcloud.supermarket.mapper;
|
||||
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelAvailSubEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SmHotelAvailSubMapper extends PjcloudBaseMapper<SmHotelAvailSubEntity> {
|
||||
}
|
||||
+22
-45
@@ -6,7 +6,6 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
@@ -15,58 +14,36 @@ public interface SmHotelMapper extends PjcloudBaseMapper<SmHotelEntity> {
|
||||
int insertBatch(@Param("list") List<SmHotelEntity> list);
|
||||
|
||||
/**
|
||||
* App 店名前缀搜索:走 idx_hotel_name,可带区划区间;带 ORDER BY 安全(前缀命中集小)。
|
||||
* 仅酒店表:店名前缀(走 idx_hotel_name)。几万级酒店 + LIMIT,秒级内。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT id, hotel_name AS hotelName, hotel_code AS hotelCode, address, district,
|
||||
district_adcode AS districtAdcode, biz_area AS bizArea, star_level AS starLevel,
|
||||
score, facility_tags AS facilityTags, cover_image AS coverImage, phone,
|
||||
sheet_name AS sheetName, upstream_status AS upstreamStatus, update_time AS updateTime
|
||||
FROM canteen_hotel FORCE INDEX (idx_hotel_name)
|
||||
WHERE del_flag = '0'
|
||||
AND (upstream_status IS NULL OR upstream_status <> 'OFFLINE')
|
||||
AND (hotel_name LIKE CONCAT(#{keyword}, '%') OR hotel_code = #{keyword})
|
||||
<if test="adFrom != null">
|
||||
AND district_adcode >= #{adFrom} AND district_adcode < #{adTo}
|
||||
</if>
|
||||
<if test="starLevels != null and starLevels.size() > 0">
|
||||
AND star_level IN
|
||||
<foreach collection="starLevels" item="s" open="(" separator="," close=")">#{s}</foreach>
|
||||
</if>
|
||||
ORDER BY IFNULL(star_level, 0) DESC, id DESC
|
||||
LIMIT #{limit}
|
||||
</script>
|
||||
""")
|
||||
List<SmHotelEntity> selectAppByNamePrefix(@Param("keyword") String keyword, @Param("adFrom") Long adFrom,
|
||||
@Param("adTo") Long adTo, @Param("starLevels") Collection<Integer> starLevels,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* App 店名包含搜索:禁止 ORDER BY(含匹配 + 排序 = 全表 filesort 超时)。
|
||||
* 先 LIMIT 取候选,由调用方内存排序。有 adcode 时扫描范围极小。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT id, hotel_name AS hotelName, hotel_code AS hotelCode, address, district,
|
||||
district_adcode AS districtAdcode, biz_area AS bizArea, star_level AS starLevel,
|
||||
score, facility_tags AS facilityTags, cover_image AS coverImage, phone,
|
||||
sheet_name AS sheetName, upstream_status AS upstreamStatus, update_time AS updateTime
|
||||
FROM canteen_hotel
|
||||
WHERE del_flag = '0'
|
||||
AND (upstream_status IS NULL OR upstream_status <> 'OFFLINE')
|
||||
AND hotel_name LIKE CONCAT('%', #{keyword}, '%')
|
||||
<if test="adFrom != null">
|
||||
AND district_adcode >= #{adFrom} AND district_adcode < #{adTo}
|
||||
</if>
|
||||
<if test="starLevels != null and starLevels.size() > 0">
|
||||
AND star_level IN
|
||||
<foreach collection="starLevels" item="s" open="(" separator="," close=")">#{s}</foreach>
|
||||
</if>
|
||||
AND (upstream_status IS NULL OR upstream_status <> 'OFFLINE')
|
||||
AND (hotel_name LIKE CONCAT(#{keyword}, '%') OR hotel_code = #{keyword})
|
||||
ORDER BY star_level DESC, id DESC
|
||||
LIMIT #{limit}
|
||||
</script>
|
||||
""")
|
||||
List<SmHotelEntity> selectAppByNameContains(@Param("keyword") String keyword, @Param("adFrom") Long adFrom,
|
||||
@Param("adTo") Long adTo, @Param("starLevels") Collection<Integer> starLevels,
|
||||
@Param("limit") int limit);
|
||||
List<SmHotelEntity> selectAppByNamePrefix(@Param("keyword") String keyword, @Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 仅酒店表:店名包含。无 ORDER BY(避免 filesort);LIMIT 后内存排序。
|
||||
* 几万酒店全表扫含匹配通常远快于带日历/区划的复杂 SQL。
|
||||
*/
|
||||
@Select("""
|
||||
SELECT id, hotel_name AS hotelName, hotel_code AS hotelCode, address, district,
|
||||
district_adcode AS districtAdcode, biz_area AS bizArea, star_level AS starLevel,
|
||||
score, facility_tags AS facilityTags, cover_image AS coverImage, phone,
|
||||
sheet_name AS sheetName, upstream_status AS upstreamStatus, update_time AS updateTime
|
||||
FROM canteen_hotel
|
||||
WHERE del_flag = '0'
|
||||
AND (upstream_status IS NULL OR upstream_status <> 'OFFLINE')
|
||||
AND hotel_name LIKE CONCAT('%', #{keyword}, '%')
|
||||
LIMIT #{limit}
|
||||
""")
|
||||
List<SmHotelEntity> selectAppByNameContains(@Param("keyword") String keyword, @Param("limit") int limit);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelAvailSubEntity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 酒店有房通知:用户订阅 CRUD + 日历变更后扫描推送。
|
||||
*/
|
||||
public interface SmHotelAvailabilityNotifyService {
|
||||
|
||||
/**
|
||||
* 订阅有房通知。
|
||||
*
|
||||
* @param watchMode RANGE=指定日期(须传 checkIn/checkOut);ANY=不限日期有房就通知
|
||||
*/
|
||||
R<SmHotelAvailSubEntity> subscribe(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode);
|
||||
|
||||
/**
|
||||
* 取消订阅(优先按 id;否则按同键)。
|
||||
*/
|
||||
R<Boolean> unsubscribe(Long userId, Long id, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode);
|
||||
|
||||
/**
|
||||
* 当前用户订阅分页。
|
||||
*/
|
||||
Page<SmHotelAvailSubEntity> pageMine(Page<?> page, Long userId);
|
||||
|
||||
/**
|
||||
* 是否已订阅:匹配指定日期 RANGE,或同产品同间数的 ANY。
|
||||
*/
|
||||
Map<String, Object> status(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms);
|
||||
|
||||
/**
|
||||
* 酒店日历/库存变化后防抖扫描 ACTIVE 订阅并推送。
|
||||
*/
|
||||
void scheduleCheckAfterHotelChanged(Long hotelId);
|
||||
|
||||
/**
|
||||
* 立即扫描指定酒店的 ACTIVE 有房订阅(运维/联调手动触发)。
|
||||
*/
|
||||
Map<String, Object> scanNow(Long hotelId);
|
||||
}
|
||||
+609
@@ -0,0 +1,609 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelAvailSubEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelAvailSubMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDailyMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelProductMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdAppMessageService;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 有房通知订阅与日历变更扫描推送。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmHotelAvailabilityNotifyServiceImpl implements SmHotelAvailabilityNotifyService {
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
private static final long DEBOUNCE_MS = 800L;
|
||||
|
||||
/** ANY 模式扫描:今天起多少天内任意一晚可订即通知 */
|
||||
private static final int ANY_LOOKAHEAD_DAYS = 30;
|
||||
|
||||
private final SmHotelAvailSubMapper availSubMapper;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
private final SmHotelProductMapper productMapper;
|
||||
|
||||
private final SmHotelDailyMapper dailyMapper;
|
||||
|
||||
private final RemoterThirdAppMessageService remoteAppMessageService;
|
||||
|
||||
private final ConcurrentHashMap<Long, ScheduledFuture<?>> pendingByHotel = new ConcurrentHashMap<>();
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "sm-hotel-avail-notify");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelAvailSubEntity> subscribe(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode) {
|
||||
if (userId == null || hotelId == null || StrUtil.isBlank(productCode)) {
|
||||
return R.failed("订阅参数不完整");
|
||||
}
|
||||
String mode = normalizeWatchMode(watchMode);
|
||||
boolean anyMode = SmHotelAvailSubEntity.WATCH_ANY.equals(mode);
|
||||
LocalDate in;
|
||||
LocalDate out;
|
||||
if (anyMode) {
|
||||
in = SmHotelAvailSubEntity.ANY_SENTINEL_IN;
|
||||
out = SmHotelAvailSubEntity.ANY_SENTINEL_OUT;
|
||||
}
|
||||
else {
|
||||
if (checkIn == null || checkOut == null) {
|
||||
return R.failed("请选择入住和离店日期");
|
||||
}
|
||||
if (!checkOut.isAfter(checkIn)) {
|
||||
return R.failed("离店日须晚于入住日");
|
||||
}
|
||||
in = checkIn;
|
||||
out = checkOut;
|
||||
}
|
||||
int need = normalizeRooms(rooms);
|
||||
SmHotelEntity hotel = hotelMapper.selectById(hotelId);
|
||||
if (hotel == null) {
|
||||
return R.failed("酒店不存在");
|
||||
}
|
||||
SmHotelProductEntity product = productMapper.selectOne(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelProductEntity::getProductCode, productCode.trim())
|
||||
.last("LIMIT 1"));
|
||||
if (product == null) {
|
||||
return R.failed("产品不存在");
|
||||
}
|
||||
if (isSatisfied(hotelId, product.getProductCode(), in, out, need, mode)) {
|
||||
return R.failed(anyMode ? "近期已有可订房间,无需订阅" : "所选日期已有房,无需订阅");
|
||||
}
|
||||
SmHotelAvailSubEntity exist = findByKey(userId, hotelId, product.getProductCode(), in, out, need, mode);
|
||||
if (exist != null) {
|
||||
if (SmHotelAvailSubEntity.STATUS_ACTIVE.equals(exist.getStatus())) {
|
||||
return R.ok(exist);
|
||||
}
|
||||
int ver = exist.getNotifyVersion() == null ? 0 : exist.getNotifyVersion();
|
||||
exist.setStatus(SmHotelAvailSubEntity.STATUS_ACTIVE);
|
||||
exist.setWatchMode(mode);
|
||||
exist.setNotifyVersion(ver + 1);
|
||||
exist.setNotifiedTime(null);
|
||||
exist.setHotelName(hotel.getHotelName());
|
||||
exist.setProductName(product.getProductName());
|
||||
availSubMapper.updateById(exist);
|
||||
log.info("|有房通知|重新激活|subId={}|mode={}|hotelId={}|product={}|", exist.getId(), mode, hotelId,
|
||||
product.getProductCode());
|
||||
return R.ok(exist);
|
||||
}
|
||||
SmHotelAvailSubEntity row = new SmHotelAvailSubEntity();
|
||||
row.setUserId(userId);
|
||||
row.setHotelId(hotelId);
|
||||
row.setProductCode(product.getProductCode());
|
||||
row.setCheckIn(in);
|
||||
row.setCheckOut(out);
|
||||
row.setRooms(need);
|
||||
row.setWatchMode(mode);
|
||||
row.setStatus(SmHotelAvailSubEntity.STATUS_ACTIVE);
|
||||
row.setNotifyVersion(0);
|
||||
row.setHotelName(hotel.getHotelName());
|
||||
row.setProductName(product.getProductName());
|
||||
availSubMapper.insert(row);
|
||||
log.info("|有房通知|新建订阅|subId={}|mode={}|userId={}|hotelId={}|product={}|{}~{}|rooms={}|",
|
||||
row.getId(), mode, userId, hotelId, product.getProductCode(), in, out, need);
|
||||
return R.ok(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> unsubscribe(Long userId, Long id, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode) {
|
||||
SmHotelAvailSubEntity row = null;
|
||||
if (id != null) {
|
||||
row = availSubMapper.selectById(id);
|
||||
}
|
||||
else if (userId != null && hotelId != null && StrUtil.isNotBlank(productCode)) {
|
||||
String mode = normalizeWatchMode(watchMode);
|
||||
LocalDate in;
|
||||
LocalDate out;
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(mode)) {
|
||||
in = SmHotelAvailSubEntity.ANY_SENTINEL_IN;
|
||||
out = SmHotelAvailSubEntity.ANY_SENTINEL_OUT;
|
||||
}
|
||||
else {
|
||||
in = checkIn;
|
||||
out = checkOut;
|
||||
}
|
||||
if (in != null && out != null) {
|
||||
row = findByKey(userId, hotelId, productCode.trim(), in, out, normalizeRooms(rooms), mode);
|
||||
}
|
||||
}
|
||||
if (row == null || !Objects.equals(row.getUserId(), userId)) {
|
||||
return R.failed("订阅不存在");
|
||||
}
|
||||
if (SmHotelAvailSubEntity.STATUS_CANCELLED.equals(row.getStatus())) {
|
||||
return R.ok(true);
|
||||
}
|
||||
row.setStatus(SmHotelAvailSubEntity.STATUS_CANCELLED);
|
||||
availSubMapper.updateById(row);
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<SmHotelAvailSubEntity> pageMine(Page<?> page, Long userId) {
|
||||
return availSubMapper.selectPage(
|
||||
new Page<>(page.getCurrent(), page.getSize()),
|
||||
Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.ne(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_CANCELLED)
|
||||
.orderByDesc(SmHotelAvailSubEntity::getUpdateTime)
|
||||
.orderByDesc(SmHotelAvailSubEntity::getId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> status(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms) {
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("subscribed", false);
|
||||
out.put("subscriptionId", null);
|
||||
out.put("watchMode", null);
|
||||
if (userId == null || hotelId == null || StrUtil.isBlank(productCode)) {
|
||||
return out;
|
||||
}
|
||||
int need = normalizeRooms(rooms);
|
||||
String code = productCode.trim();
|
||||
// 优先:当前页筛选日期的 RANGE
|
||||
if (checkIn != null && checkOut != null) {
|
||||
SmHotelAvailSubEntity range = availSubMapper.selectOne(Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getProductCode, code)
|
||||
.eq(SmHotelAvailSubEntity::getCheckIn, checkIn)
|
||||
.eq(SmHotelAvailSubEntity::getCheckOut, checkOut)
|
||||
.eq(SmHotelAvailSubEntity::getRooms, need)
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE)
|
||||
.and(w -> w.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_RANGE)
|
||||
.or()
|
||||
.isNull(SmHotelAvailSubEntity::getWatchMode))
|
||||
.last("LIMIT 1"));
|
||||
if (range != null) {
|
||||
out.put("subscribed", true);
|
||||
out.put("subscriptionId", range.getId());
|
||||
out.put("watchMode", SmHotelAvailSubEntity.WATCH_RANGE);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
// 其次:同产品同间数的 ANY(不限日期)
|
||||
SmHotelAvailSubEntity any = availSubMapper.selectOne(Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getProductCode, code)
|
||||
.eq(SmHotelAvailSubEntity::getRooms, need)
|
||||
.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_ANY)
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE)
|
||||
.last("LIMIT 1"));
|
||||
if (any != null) {
|
||||
out.put("subscribed", true);
|
||||
out.put("subscriptionId", any.getId());
|
||||
out.put("watchMode", SmHotelAvailSubEntity.WATCH_ANY);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleCheckAfterHotelChanged(Long hotelId) {
|
||||
if (hotelId == null) {
|
||||
return;
|
||||
}
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
Long tid = tenantId;
|
||||
log.info("|有房通知|安排扫描|hotelId={}|tenantId={}|txActive={}|", hotelId, tid,
|
||||
TransactionSynchronizationManager.isSynchronizationActive());
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
// 提交后短延迟再扫,确保日历行对查询可见;同酒店合并多次触发
|
||||
scheduleDebounced(hotelId, tid);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
scheduleDebounced(hotelId, tid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> scanNow(Long hotelId) {
|
||||
if (hotelId == null) {
|
||||
Map<String, Object> empty = new LinkedHashMap<>();
|
||||
empty.put("hotelId", null);
|
||||
empty.put("activeSubs", 0);
|
||||
empty.put("notified", 0);
|
||||
empty.put("skippedNotBookable", 0);
|
||||
empty.put("feignFailed", 0);
|
||||
return empty;
|
||||
}
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
return TenantBroker.applyAs(tenantId, t -> doScanAndNotify(hotelId));
|
||||
}
|
||||
|
||||
private void scheduleDebounced(Long hotelId, Long tenantId) {
|
||||
Long tid = tenantId == null ? 1L : tenantId;
|
||||
ScheduledFuture<?> prev = pendingByHotel.get(hotelId);
|
||||
if (prev != null) {
|
||||
prev.cancel(false);
|
||||
}
|
||||
ScheduledFuture<?> future = scheduler.schedule(() -> {
|
||||
pendingByHotel.remove(hotelId);
|
||||
try {
|
||||
log.info("|有房通知|开始异步扫描|hotelId={}|tenantId={}|", hotelId, tid);
|
||||
TenantBroker.runAs(tid, t -> doScanAndNotify(hotelId));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|有房通知扫描失败|hotelId={}|{}", hotelId, e.getMessage(), e);
|
||||
}
|
||||
}, DEBOUNCE_MS, TimeUnit.MILLISECONDS);
|
||||
pendingByHotel.put(hotelId, future);
|
||||
}
|
||||
|
||||
private Map<String, Object> doScanAndNotify(Long hotelId) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("hotelId", hotelId);
|
||||
List<SmHotelAvailSubEntity> subs = availSubMapper.selectList(Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE)
|
||||
.last("LIMIT 500"));
|
||||
int active = CollUtil.isEmpty(subs) ? 0 : subs.size();
|
||||
summary.put("activeSubs", active);
|
||||
int notified = 0;
|
||||
int skipped = 0;
|
||||
int feignFailed = 0;
|
||||
if (CollUtil.isEmpty(subs)) {
|
||||
log.info("|有房通知|扫描无ACTIVE订阅|hotelId={}|", hotelId);
|
||||
summary.put("notified", 0);
|
||||
summary.put("skippedNotBookable", 0);
|
||||
summary.put("feignFailed", 0);
|
||||
summary.put("hint", "该酒店没有 ACTIVE 订阅,请确认 App 已订阅成功且表 canteen_hotel_avail_sub.status=ACTIVE");
|
||||
return summary;
|
||||
}
|
||||
List<Map<String, Object>> subBrief = new ArrayList<>();
|
||||
for (SmHotelAvailSubEntity s : subs) {
|
||||
Map<String, Object> b = new LinkedHashMap<>();
|
||||
b.put("id", s.getId());
|
||||
b.put("productCode", s.getProductCode());
|
||||
b.put("watchMode", resolveWatchMode(s));
|
||||
b.put("rooms", s.getRooms());
|
||||
b.put("checkIn", s.getCheckIn() == null ? null : s.getCheckIn().toString());
|
||||
b.put("checkOut", s.getCheckOut() == null ? null : s.getCheckOut().toString());
|
||||
subBrief.add(b);
|
||||
}
|
||||
summary.put("subscriptions", subBrief);
|
||||
log.info("|有房通知|开始扫描|hotelId={}|activeSubs={}|subs={}|", hotelId, active, subBrief);
|
||||
for (SmHotelAvailSubEntity sub : subs) {
|
||||
try {
|
||||
String result = notifyIfReady(sub);
|
||||
switch (result) {
|
||||
case "NOTIFIED" -> notified++;
|
||||
case "NOT_BOOKABLE" -> skipped++;
|
||||
case "FEIGN_FAILED" -> feignFailed++;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
feignFailed++;
|
||||
log.warn("|有房通知单条失败|subId={}|{}", sub.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
summary.put("notified", notified);
|
||||
summary.put("skippedNotBookable", skipped);
|
||||
summary.put("feignFailed", feignFailed);
|
||||
log.info("|有房通知|扫描结束|hotelId={}|notified={}|skippedNotBookable={}|feignFailed={}|",
|
||||
hotelId, notified, skipped, feignFailed);
|
||||
return summary;
|
||||
}
|
||||
|
||||
private String notifyIfReady(SmHotelAvailSubEntity sub) {
|
||||
if (sub == null || sub.getId() == null) {
|
||||
return "SKIP";
|
||||
}
|
||||
// 兼容:库中 watch_mode 未写入时,用占位日期识别 ANY
|
||||
String mode = resolveWatchMode(sub);
|
||||
int need = normalizeRooms(sub.getRooms());
|
||||
log.info("|有房通知|检查订阅|subId={}|mode={}|dbWatchMode={}|product={}|{}~{}|rooms={}|",
|
||||
sub.getId(), mode, sub.getWatchMode(), sub.getProductCode(),
|
||||
sub.getCheckIn(), sub.getCheckOut(), need);
|
||||
if (!isSatisfied(sub.getHotelId(), sub.getProductCode(), sub.getCheckIn(), sub.getCheckOut(), need, mode)) {
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(mode)) {
|
||||
LocalDate from = LocalDate.now();
|
||||
LocalDate toEx = from.plusDays(ANY_LOOKAHEAD_DAYS);
|
||||
log.info("|有房通知|ANY暂无可订跳过|subId={}|product={}|window={}~{}|rooms={}|",
|
||||
sub.getId(), sub.getProductCode(), from, toEx, need);
|
||||
}
|
||||
else {
|
||||
List<LocalDate> missing = missingBookableNights(sub.getHotelId(), sub.getProductCode(),
|
||||
sub.getCheckIn(), sub.getCheckOut(), need);
|
||||
log.info("|有房通知|RANGE暂不可订跳过|subId={}|product={}|{}~{}|missingNights={}|",
|
||||
sub.getId(), sub.getProductCode(), sub.getCheckIn(), sub.getCheckOut(),
|
||||
missing.stream().map(LocalDate::toString).collect(Collectors.joining(",")));
|
||||
}
|
||||
return "NOT_BOOKABLE";
|
||||
}
|
||||
int ver = sub.getNotifyVersion() == null ? 0 : sub.getNotifyVersion();
|
||||
String eventId = "HOTEL_ROOM:" + sub.getId() + ":" + ver;
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = sub.getTenantId();
|
||||
}
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
String hotelName = StrUtil.blankToDefault(sub.getHotelName(), "酒店");
|
||||
String productName = StrUtil.blankToDefault(sub.getProductName(), sub.getProductCode());
|
||||
HotelRoomAvailableEventDto dto = new HotelRoomAvailableEventDto();
|
||||
dto.setEventId(eventId);
|
||||
dto.setTenantId(tenantId);
|
||||
dto.setRecipientUserId(sub.getUserId());
|
||||
dto.setHotelId(sub.getHotelId());
|
||||
dto.setProductCode(sub.getProductCode());
|
||||
dto.setSourceName(hotelName);
|
||||
dto.setTitle("有房提醒");
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(mode)) {
|
||||
dto.setContent(hotelName + "「" + productName + "」近期已有空房,可前往查看预订");
|
||||
}
|
||||
else {
|
||||
dto.setContent(hotelName + "「" + productName + "」"
|
||||
+ sub.getCheckIn().format(DAY) + "至" + sub.getCheckOut().format(DAY)
|
||||
+ "已有空房,可前往预订");
|
||||
}
|
||||
Map<String, Object> payload = new HashMap<>(8);
|
||||
payload.put("hotelId", sub.getHotelId());
|
||||
payload.put("productCode", sub.getProductCode());
|
||||
payload.put("watchMode", mode);
|
||||
payload.put("rooms", need);
|
||||
payload.put("subscriptionId", sub.getId());
|
||||
if (SmHotelAvailSubEntity.WATCH_RANGE.equals(mode)) {
|
||||
payload.put("checkIn", sub.getCheckIn().toString());
|
||||
payload.put("checkOut", sub.getCheckOut().toString());
|
||||
}
|
||||
dto.setPayloadJson(JSONUtil.toJsonStr(payload));
|
||||
dto.setDeepLinkPath("/pages/hotel/detail");
|
||||
Map<String, Object> linkParams = new HashMap<>(8);
|
||||
linkParams.put("id", String.valueOf(sub.getHotelId()));
|
||||
linkParams.put("rooms", String.valueOf(need));
|
||||
if (SmHotelAvailSubEntity.WATCH_RANGE.equals(mode) && sub.getCheckIn() != null && sub.getCheckOut() != null) {
|
||||
linkParams.put("checkIn", sub.getCheckIn().toString());
|
||||
linkParams.put("checkOut", sub.getCheckOut().toString());
|
||||
}
|
||||
dto.setDeepLinkParamsJson(JSONUtil.toJsonStr(linkParams));
|
||||
dto.setOccurredTime(LocalDateTime.now());
|
||||
log.info("|有房通知|调用Feign入箱|subId={}|mode={}|eventId={}|userId={}|",
|
||||
sub.getId(), mode, eventId, sub.getUserId());
|
||||
R<Boolean> resp;
|
||||
try {
|
||||
resp = remoteAppMessageService.receiveHotelRoomAvailableEvent(dto);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|有房通知|Feign异常|subId={}|err={}|", sub.getId(), e.getMessage(), e);
|
||||
return "FEIGN_FAILED";
|
||||
}
|
||||
if (resp == null || !resp.isOk() || !Boolean.TRUE.equals(resp.getData())) {
|
||||
log.warn("|有房通知Feign未成功|subId={}|code={}|msg={}|",
|
||||
sub.getId(), resp == null ? null : resp.getCode(), resp == null ? "null" : resp.getMsg());
|
||||
return "FEIGN_FAILED";
|
||||
}
|
||||
SmHotelAvailSubEntity upd = new SmHotelAvailSubEntity();
|
||||
upd.setId(sub.getId());
|
||||
upd.setStatus(SmHotelAvailSubEntity.STATUS_NOTIFIED);
|
||||
upd.setNotifiedTime(LocalDateTime.now());
|
||||
int updated = availSubMapper.update(upd, Wrappers.<SmHotelAvailSubEntity>lambdaUpdate()
|
||||
.eq(SmHotelAvailSubEntity::getId, sub.getId())
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE));
|
||||
log.info("|有房通知|入箱成功并标记NOTIFIED|subId={}|mode={}|updated={}|", sub.getId(), mode, updated);
|
||||
return "NOTIFIED";
|
||||
}
|
||||
|
||||
private boolean isSatisfied(Long hotelId, String productCode, LocalDate checkIn, LocalDate checkOut,
|
||||
int rooms, String watchMode) {
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(normalizeWatchMode(watchMode))) {
|
||||
return hasAnyBookableNight(hotelId, productCode, rooms);
|
||||
}
|
||||
// RANGE 但落了 ANY 占位日期时,按 ANY 处理,避免永远扫不到
|
||||
if (isAnySentinel(checkIn, checkOut)) {
|
||||
return hasAnyBookableNight(hotelId, productCode, rooms);
|
||||
}
|
||||
return missingBookableNights(hotelId, productCode, checkIn, checkOut, rooms).isEmpty()
|
||||
&& checkIn != null && checkOut != null && checkOut.isAfter(checkIn);
|
||||
}
|
||||
|
||||
/** 近 ANY_LOOKAHEAD_DAYS 天内是否存在任意一晚可订 */
|
||||
private boolean hasAnyBookableNight(Long hotelId, String productCode, int rooms) {
|
||||
if (hotelId == null || StrUtil.isBlank(productCode)) {
|
||||
return false;
|
||||
}
|
||||
LocalDate from = LocalDate.now();
|
||||
LocalDate toEx = from.plusDays(ANY_LOOKAHEAD_DAYS);
|
||||
List<SmHotelDailyEntity> rows = dailyMapper.selectList(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelDailyEntity::getProductCode, productCode)
|
||||
.ge(SmHotelDailyEntity::getStayDate, from)
|
||||
.lt(SmHotelDailyEntity::getStayDate, toEx));
|
||||
int rowCnt = rows == null ? 0 : rows.size();
|
||||
int openCnt = 0;
|
||||
if (rows != null) {
|
||||
for (SmHotelDailyEntity row : rows) {
|
||||
if (row == null || row.getStayDate() == null) {
|
||||
continue;
|
||||
}
|
||||
int status = row.getDailyStatus() == null ? 1 : row.getDailyStatus();
|
||||
int stock = row.getStock() == null ? 0 : row.getStock();
|
||||
int sold = row.getSold() == null ? 0 : row.getSold();
|
||||
if (status == 1 && stock - sold >= rooms) {
|
||||
openCnt++;
|
||||
log.info("|有房通知|ANY命中可订日|hotelId={}|product={}|stayDate={}|stock={}|sold={}|need={}|",
|
||||
hotelId, productCode, row.getStayDate(), stock, sold, rooms);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("|有房通知|ANY窗口无命中|hotelId={}|product={}|window={}~{}|dailyRows={}|openEnough={}|need={}|",
|
||||
hotelId, productCode, from, toEx, rowCnt, openCnt, rooms);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isAnySentinel(LocalDate checkIn, LocalDate checkOut) {
|
||||
return SmHotelAvailSubEntity.ANY_SENTINEL_IN.equals(checkIn)
|
||||
&& SmHotelAvailSubEntity.ANY_SENTINEL_OUT.equals(checkOut);
|
||||
}
|
||||
|
||||
private static String resolveWatchMode(SmHotelAvailSubEntity sub) {
|
||||
if (sub == null) {
|
||||
return SmHotelAvailSubEntity.WATCH_RANGE;
|
||||
}
|
||||
if (isAnySentinel(sub.getCheckIn(), sub.getCheckOut())) {
|
||||
return SmHotelAvailSubEntity.WATCH_ANY;
|
||||
}
|
||||
return normalizeWatchMode(sub.getWatchMode());
|
||||
}
|
||||
|
||||
private List<LocalDate> missingBookableNights(Long hotelId, String productCode, LocalDate checkIn,
|
||||
LocalDate checkOut, int rooms) {
|
||||
List<LocalDate> missing = new ArrayList<>();
|
||||
if (hotelId == null || StrUtil.isBlank(productCode) || checkIn == null || checkOut == null
|
||||
|| !checkOut.isAfter(checkIn)) {
|
||||
if (checkIn != null) {
|
||||
missing.add(checkIn);
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
List<LocalDate> nights = new ArrayList<>();
|
||||
for (LocalDate d = checkIn; d.isBefore(checkOut); d = d.plusDays(1)) {
|
||||
nights.add(d);
|
||||
}
|
||||
List<SmHotelDailyEntity> rows = dailyMapper.selectList(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelDailyEntity::getProductCode, productCode)
|
||||
.ge(SmHotelDailyEntity::getStayDate, checkIn)
|
||||
.lt(SmHotelDailyEntity::getStayDate, checkOut));
|
||||
Set<LocalDate> ok = new HashSet<>();
|
||||
if (rows != null) {
|
||||
for (SmHotelDailyEntity row : rows) {
|
||||
if (row == null || row.getStayDate() == null) {
|
||||
continue;
|
||||
}
|
||||
int status = row.getDailyStatus() == null ? 1 : row.getDailyStatus();
|
||||
int stock = row.getStock() == null ? 0 : row.getStock();
|
||||
int sold = row.getSold() == null ? 0 : row.getSold();
|
||||
if (status == 1 && stock - sold >= rooms) {
|
||||
ok.add(row.getStayDate());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (LocalDate night : nights) {
|
||||
if (!ok.contains(night)) {
|
||||
missing.add(night);
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
private SmHotelAvailSubEntity findByKey(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, int rooms, String watchMode) {
|
||||
var q = Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getProductCode, productCode)
|
||||
.eq(SmHotelAvailSubEntity::getCheckIn, checkIn)
|
||||
.eq(SmHotelAvailSubEntity::getCheckOut, checkOut)
|
||||
.eq(SmHotelAvailSubEntity::getRooms, rooms)
|
||||
.last("LIMIT 1");
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(watchMode)) {
|
||||
q.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_ANY);
|
||||
}
|
||||
else {
|
||||
q.and(w -> w.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_RANGE)
|
||||
.or()
|
||||
.isNull(SmHotelAvailSubEntity::getWatchMode));
|
||||
}
|
||||
return availSubMapper.selectOne(q);
|
||||
}
|
||||
|
||||
private static String normalizeWatchMode(String watchMode) {
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equalsIgnoreCase(StrUtil.trim(watchMode))) {
|
||||
return SmHotelAvailSubEntity.WATCH_ANY;
|
||||
}
|
||||
return SmHotelAvailSubEntity.WATCH_RANGE;
|
||||
}
|
||||
|
||||
private static int normalizeRooms(Integer rooms) {
|
||||
if (rooms == null || rooms < 1) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min(rooms, 20);
|
||||
}
|
||||
}
|
||||
+63
-3
@@ -22,6 +22,7 @@ import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamPushLogConte
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelClient;
|
||||
import com.bsd.pjcloud.supermarket.mapper.*;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAutoPushService;
|
||||
@@ -91,6 +92,9 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
|
||||
private final SmHotelOpenResourceNotifyService openResourceNotifyService;
|
||||
|
||||
/** 有房通知扫描(日历变更后防抖)。 */
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
@@ -106,7 +110,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
SmHotelPriceConfigService priceConfigService, SmHotelChannelConfigService channelConfigService,
|
||||
SmHotelChannelDeptService channelDeptService, SmHotelDownstreamPushLogService downstreamPushLogService,
|
||||
@Lazy SmHotelDownstreamAutoPushService downstreamAutoPushService,
|
||||
@Lazy SmHotelOpenResourceNotifyService openResourceNotifyService, SmHotelMapper hotelMapper,
|
||||
@Lazy SmHotelOpenResourceNotifyService openResourceNotifyService,
|
||||
@Lazy SmHotelAvailabilityNotifyService availabilityNotifyService, SmHotelMapper hotelMapper,
|
||||
SmHotelRoomMapper roomMapper, SmHotelProductMapper productMapper, SmHotelDailyMapper dailyMapper,
|
||||
StringRedisTemplate stringRedisTemplate) {
|
||||
this.upstreamClient = upstreamClient;
|
||||
@@ -119,6 +124,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
this.downstreamPushLogService = downstreamPushLogService;
|
||||
this.downstreamAutoPushService = downstreamAutoPushService;
|
||||
this.openResourceNotifyService = openResourceNotifyService;
|
||||
this.availabilityNotifyService = availabilityNotifyService;
|
||||
this.hotelMapper = hotelMapper;
|
||||
this.roomMapper = roomMapper;
|
||||
this.productMapper = productMapper;
|
||||
@@ -225,6 +231,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
log.info("|酒店拉取上游|结束|hotelCode={}|room={}|product={}|daily={}|tips={}|", hotelCode, roomCount,
|
||||
productCount, dailyCount, upstreamTips);
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@@ -353,6 +360,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
result.put("endExclusive", endExclusive.toString());
|
||||
// 回调区间拉价 / 手动滚价后,防抖推下游
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@@ -838,6 +846,37 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
return 20;
|
||||
}
|
||||
|
||||
/** 窗型文案 → 环启 roomWindow:0无窗/1有窗/2部分有窗/99其他 */
|
||||
private static int mapRoomWindowCode(String windowType) {
|
||||
if (StrUtil.isBlank(windowType)) {
|
||||
return 99;
|
||||
}
|
||||
String t = windowType.trim();
|
||||
if (t.contains("部分")) {
|
||||
return 2;
|
||||
}
|
||||
if (t.contains("无窗") || "0".equals(t)) {
|
||||
return 0;
|
||||
}
|
||||
if (t.contains("有窗") || "1".equals(t)) {
|
||||
return 1;
|
||||
}
|
||||
return 99;
|
||||
}
|
||||
|
||||
/** 上游 roomWindow 码 → 本地窗型文案 */
|
||||
private static String mapWindowTypeLabel(Integer roomWindow) {
|
||||
if (roomWindow == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (roomWindow) {
|
||||
case 0 -> "无窗";
|
||||
case 1 -> "有窗";
|
||||
case 2 -> "部分有窗";
|
||||
default -> "其他";
|
||||
};
|
||||
}
|
||||
|
||||
/** 按产品推整窗关房区间;返回推送的 list 段数 */
|
||||
private int pushCloseDailyRanges(String partnerCode, SmHotelEntity hotel, List<SmHotelProductEntity> products,
|
||||
BigDecimal defaultPrice) {
|
||||
@@ -1041,8 +1080,11 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
item.put("extApartmentId", room.getRoomTypeCode());
|
||||
item.put("apartmentName", StrUtil.blankToDefault(room.getRoomTypeName(), room.getRoomTypeCode()));
|
||||
item.put("bedType", room.getBedType());
|
||||
// 环启 roomWindow:0无窗/1有窗/2部分有窗/99其他;本地无字段时按 99
|
||||
item.put("roomWindow", 99);
|
||||
item.put("floorInfo", room.getFloorInfo());
|
||||
item.put("areaSqm", room.getAreaSqm());
|
||||
item.put("windowType", room.getWindowType());
|
||||
// 环启 roomWindow:0无窗/1有窗/2部分有窗/99其他
|
||||
item.put("roomWindow", mapRoomWindowCode(room.getWindowType()));
|
||||
int capacity = room.getCapacity() == null ? 2 : room.getCapacity();
|
||||
item.put("capacity", capacity);
|
||||
item.put("apartmentNumber", roomQtyMap.getOrDefault(room.getRoomTypeCode(), 1));
|
||||
@@ -1680,6 +1722,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
SmHotelDailyEntity saved = dailyMapper.selectById(daily.getId());
|
||||
fillDailyRemain(saved);
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(saved);
|
||||
}
|
||||
|
||||
@@ -1694,6 +1737,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
boolean ok = dailyMapper.deleteById(dailyId) > 0;
|
||||
if (ok) {
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
}
|
||||
return R.ok(ok);
|
||||
}
|
||||
@@ -1819,6 +1863,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
? String.format("已补齐 %d 天行,跳过已有 %d", toInsert.size(), skippedExist)
|
||||
: String.format("已调整 %d 行,新补齐 %d,配额不足跳过 %d", toUpdate.size(), toInsert.size(), skippedStock);
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(data, msg);
|
||||
}
|
||||
|
||||
@@ -1959,6 +2004,21 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
room.setBedType(item.getString("bedType"));
|
||||
room.setCapacity(item.getInteger("maxCheckInPeoNum"));
|
||||
room.setRoomNumber(item.getInteger("roomNum"));
|
||||
String floor = StrUtil.blankToDefault(item.getString("floorInfo"), item.getString("floor"));
|
||||
room.setFloorInfo(StrUtil.blankToDefault(floor, null));
|
||||
BigDecimal area = item.getBigDecimal("areaSqm");
|
||||
if (area == null) {
|
||||
area = item.getBigDecimal("area");
|
||||
}
|
||||
if (area == null) {
|
||||
area = item.getBigDecimal("roomArea");
|
||||
}
|
||||
room.setAreaSqm(area);
|
||||
String windowType = item.getString("windowType");
|
||||
if (StrUtil.isBlank(windowType)) {
|
||||
windowType = mapWindowTypeLabel(item.getInteger("roomWindow"));
|
||||
}
|
||||
room.setWindowType(StrUtil.blankToDefault(windowType, null));
|
||||
room.setRawJson(item.toJSONString());
|
||||
room.setUpdateTime(now);
|
||||
if (exist == null) {
|
||||
|
||||
+64
@@ -19,6 +19,7 @@ import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrder
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrderResult;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouBookStatus;
|
||||
import com.bsd.pjcloud.supermarket.mapper.*;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
@@ -26,6 +27,8 @@ import com.bsd.pjcloud.supermarket.service.SmHotelOrderUpstreamLogService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderDetailVo;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysDept;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
@@ -108,6 +111,10 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
|
||||
private final RemoteDeptService remoteDeptService;
|
||||
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
@Override
|
||||
public Page<SmHotelOrderEntity> pageOrders(Page<SmHotelOrderEntity> page, SmHotelOrderEntity query) {
|
||||
Page<SmHotelOrderEntity> result = baseMapper.selectPage(page, Wrappers.<SmHotelOrderEntity>lambdaQuery()
|
||||
@@ -343,6 +350,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
if (hotel != null) {
|
||||
o.setHotelName(hotel.getHotelName());
|
||||
o.setHotelDataSource(upstreamGatewayRegistry.resolveChannelKey(hotel.getSheetName()));
|
||||
o.setDeptId(hotel.getDeptId());
|
||||
}
|
||||
SmHotelProductEntity product = null;
|
||||
if (StrUtil.isNotBlank(o.getProductCode())) {
|
||||
@@ -380,6 +388,39 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
fillOrderSourceFields(o, app, partnerNameMap);
|
||||
}
|
||||
fillOrderDeptNames(records);
|
||||
}
|
||||
|
||||
/** 批量补全酒店归属部门名(结算商户) */
|
||||
private void fillOrderDeptNames(List<SmHotelOrderEntity> records) {
|
||||
List<Long> deptIds = records.stream()
|
||||
.map(SmHotelOrderEntity::getDeptId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (deptIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> nameById = new HashMap<>();
|
||||
try {
|
||||
R<Map<Long, SysDept>> r = remoteDeptService.getDeptByDeptIds(deptIds);
|
||||
if (r != null && r.isOk() && r.getData() != null) {
|
||||
r.getData().forEach((id, d) -> {
|
||||
if (id != null && d != null && StrUtil.isNotBlank(d.getName())) {
|
||||
nameById.put(id, d.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店订单|补全部门名失败|{}", e.getMessage());
|
||||
}
|
||||
for (SmHotelOrderEntity o : records) {
|
||||
if (o.getDeptId() == null) {
|
||||
continue;
|
||||
}
|
||||
o.setDeptName(nameById.getOrDefault(o.getDeptId(), String.valueOf(o.getDeptId())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void indexOpenAppNames(Map<String, String> partnerNameMap,
|
||||
@@ -522,10 +563,31 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
hb.setLongitude(hotel.getLongitude());
|
||||
hb.setLatitude(hotel.getLatitude());
|
||||
hb.setDeptId(hotel.getDeptId());
|
||||
if (hotel.getDeptId() != null) {
|
||||
try {
|
||||
R<Map<Long, SysDept>> r = remoteDeptService.getDeptByDeptIds(List.of(hotel.getDeptId()));
|
||||
if (r != null && r.isOk() && r.getData() != null) {
|
||||
SysDept d = r.getData().get(hotel.getDeptId());
|
||||
if (d != null && StrUtil.isNotBlank(d.getName())) {
|
||||
hb.setDeptName(d.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店订单详情|补全部门名失败|deptId={}|{}", hotel.getDeptId(), e.getMessage());
|
||||
}
|
||||
if (StrUtil.isBlank(hb.getDeptName())) {
|
||||
hb.setDeptName(String.valueOf(hotel.getDeptId()));
|
||||
}
|
||||
}
|
||||
vo.setHotel(hb);
|
||||
if (StrUtil.isBlank(order.getHotelName())) {
|
||||
order.setHotelName(hotel.getHotelName());
|
||||
}
|
||||
if (order.getDeptId() == null) {
|
||||
order.setDeptId(hotel.getDeptId());
|
||||
order.setDeptName(hb.getDeptName());
|
||||
}
|
||||
}
|
||||
|
||||
SmHotelProductEntity product = null;
|
||||
@@ -1869,6 +1931,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
}
|
||||
order.setInventoryStatus(SmHotelInventoryStatus.ROLLED_BACK);
|
||||
// 回补后可能重新可订,触发有房通知扫描
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(order.getHotelId());
|
||||
}
|
||||
|
||||
private BigDecimal sumLocalSellAmount(Long productId, LocalDate checkIn, LocalDate checkOut, int rooms) {
|
||||
|
||||
+2
-1
@@ -5,13 +5,14 @@
|
||||
<insert id="insertBatch">
|
||||
INSERT INTO canteen_hotel_room (
|
||||
id, tenant_id, hotel_id, group_code, hotel_code, room_type_code, room_type_name,
|
||||
bed_type, capacity, room_number, raw_json,
|
||||
bed_type, capacity, room_number, floor_info, area_sqm, window_type, raw_json,
|
||||
create_by, create_time, update_by, update_time, del_flag
|
||||
) VALUES
|
||||
<foreach collection="list" item="e" separator=",">
|
||||
(
|
||||
#{e.id}, #{e.tenantId}, #{e.hotelId}, #{e.groupCode}, #{e.hotelCode},
|
||||
#{e.roomTypeCode}, #{e.roomTypeName}, #{e.bedType}, #{e.capacity}, #{e.roomNumber},
|
||||
#{e.floorInfo}, #{e.areaSqm}, #{e.windowType},
|
||||
#{e.rawJson}, #{e.createBy}, #{e.createTime}, #{e.updateBy}, #{e.updateTime}, #{e.delFlag}
|
||||
)
|
||||
</foreach>
|
||||
|
||||
+11
@@ -3,6 +3,7 @@ package com.bsd.pjcloud.thirdparty.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.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.MealVerifiedEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.WalletMessageEventDto;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -37,4 +38,14 @@ public interface RemoterThirdAppMessageService {
|
||||
@NoToken
|
||||
@PostMapping("/app-message/inner/meal-verified-event")
|
||||
R<Boolean> receiveMealVerifiedEvent(@Valid @RequestBody MealVerifiedEventDto dto);
|
||||
|
||||
/**
|
||||
* 幂等接收酒店有房通知(旁路,不进商户策略矩阵)。
|
||||
*
|
||||
* @param dto 超市酒店订阅触发的事件快照
|
||||
* @return true表示消息中心已幂等接收
|
||||
*/
|
||||
@NoToken
|
||||
@PostMapping("/app-message/inner/hotel-room-available-event")
|
||||
R<Boolean> receiveHotelRoomAvailableEvent(@Valid @RequestBody HotelRoomAvailableEventDto dto);
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.bsd.pjcloud.thirdparty.dto.appmessage;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 超市酒店服务提交的有房通知事件;旁路入箱,不走商户策略与关系订阅。
|
||||
*/
|
||||
@Data
|
||||
public class HotelRoomAvailableEventDto implements Serializable {
|
||||
|
||||
/** 跨服务幂等事件ID,建议 HOTEL_ROOM:{subId}:{notifyVersion}。 */
|
||||
@NotBlank(message = "消息事件ID不能为空")
|
||||
private String eventId;
|
||||
|
||||
/** 事件所属租户ID。 */
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
|
||||
/** 订阅用户ID(本人接收)。 */
|
||||
@NotNull(message = "接收用户ID不能为空")
|
||||
private Long recipientUserId;
|
||||
|
||||
/** 酒店ID,写入 payload / 深链。 */
|
||||
@NotNull(message = "酒店ID不能为空")
|
||||
private Long hotelId;
|
||||
|
||||
/** 产品编码快照。 */
|
||||
private String productCode;
|
||||
|
||||
/** 展示标题。 */
|
||||
@NotBlank(message = "标题不能为空")
|
||||
private String title;
|
||||
|
||||
/** 展示正文。 */
|
||||
@NotBlank(message = "正文不能为空")
|
||||
private String content;
|
||||
|
||||
/** 来源名称快照(酒店名),写入 sourceMchName 供列表展示。 */
|
||||
private String sourceName;
|
||||
|
||||
/** 详情 payload JSON;可为空。 */
|
||||
private String payloadJson;
|
||||
|
||||
/** App 深链路径,默认消息详情;可指向酒店详情。 */
|
||||
private String deepLinkPath;
|
||||
|
||||
/** App 深链参数 JSON。 */
|
||||
private String deepLinkParamsJson;
|
||||
|
||||
/** 业务发生时间。 */
|
||||
@NotNull(message = "发生时间不能为空")
|
||||
private LocalDateTime occurredTime;
|
||||
}
|
||||
+11
@@ -37,12 +37,23 @@ public class AppMessageConstants {
|
||||
/** PC人工通知来源服务,区别于自动钱包和用餐事件。 */
|
||||
public static final String SOURCE_THIRD = "THIRD";
|
||||
|
||||
/** 超市酒店业务来源服务,用于有房通知等旁路事件。 */
|
||||
public static final String SOURCE_SUPERMARKET = "SUPERMARKET";
|
||||
|
||||
/** 手动推送的稳定事件编码,只用于消息和投递,不加入自动事件策略或关系订阅矩阵。 */
|
||||
public static final String EVENT_MANUAL_PUSH = "MANUAL_PUSH";
|
||||
|
||||
/**
|
||||
* 酒店有房通知事件编码,旁路入箱,不加入 AppMessageEventEnum / 商户策略矩阵 / 关系订阅。
|
||||
*/
|
||||
public static final String EVENT_HOTEL_ROOM_AVAILABLE = "HOTEL_ROOM_AVAILABLE";
|
||||
|
||||
/** 手动通知在App中的展示分类,跨端协议值为system。 */
|
||||
public static final String CATEGORY_SYSTEM = "system";
|
||||
|
||||
/** 酒店类通知在App中的展示分类。 */
|
||||
public static final String CATEGORY_HOTEL = "hotel";
|
||||
|
||||
/** 新建后等待供应商投递的状态。 */
|
||||
public static final String DELIVERY_PENDING = "PENDING";
|
||||
|
||||
|
||||
+17
@@ -9,10 +9,12 @@ import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppRelationSubscriptionBatchSaveDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppRelationSubscriptionSaveDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppUserPushSettingSaveDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.MealVerifiedEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.WalletMessageEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdAppMessageService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdAppMessageSettingService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdHotelRoomNotifyService;
|
||||
import com.bsd.pjcloud.thirdparty.vo.appmessage.AppMessageEventOptionVo;
|
||||
import com.bsd.pjcloud.thirdparty.vo.appmessage.AppMessageVo;
|
||||
import com.bsd.pjcloud.thirdparty.vo.appmessage.AppRelationSubscriptionVo;
|
||||
@@ -50,6 +52,9 @@ public class ThirdAppMessageController {
|
||||
/** 用户Push偏好和关系订阅服务。 */
|
||||
private final ThirdAppMessageSettingService settingService;
|
||||
|
||||
/** 酒店有房通知旁路入箱。 */
|
||||
private final ThirdHotelRoomNotifyService hotelRoomNotifyService;
|
||||
|
||||
/**
|
||||
* 内部幂等接收Wallet Outbox聚合事件。
|
||||
*
|
||||
@@ -74,6 +79,18 @@ public class ThirdAppMessageController {
|
||||
return R.ok(TenantBroker.applyAs(dto.getTenantId(), tenantId -> messageService.receiveMealVerifiedEvent(dto)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部幂等接收酒店有房通知(不进入商户策略/关系订阅矩阵)。
|
||||
*
|
||||
* @param dto 超市酒店事件快照
|
||||
* @return true表示消息中心已完整接收
|
||||
*/
|
||||
@Inner
|
||||
@PostMapping("/inner/hotel-room-available-event")
|
||||
public R<Boolean> receiveHotelRoomAvailableEvent(@Valid @RequestBody HotelRoomAvailableEventDto dto) {
|
||||
return R.ok(TenantBroker.applyAs(dto.getTenantId(), tenantId -> hotelRoomNotifyService.receive(dto)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前用户全部或未读消息。
|
||||
*
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.bsd.pjcloud.thirdparty.service.appmessage;
|
||||
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
|
||||
/**
|
||||
* 酒店有房通知旁路入箱:只写本人消息并按用户 Push 总开关创建投递,不读商户策略、不扇出关系消息。
|
||||
*/
|
||||
public interface ThirdHotelRoomNotifyService {
|
||||
|
||||
/**
|
||||
* 幂等接收有房通知事件。
|
||||
*
|
||||
* @param dto 超市模块提交的事件快照
|
||||
* @return true 表示已入箱或此前已幂等完成
|
||||
*/
|
||||
boolean receive(HotelRoomAvailableEventDto dto);
|
||||
}
|
||||
+9
-2
@@ -625,8 +625,15 @@ public class ThirdAppMessageServiceImpl implements ThirdAppMessageService {
|
||||
AppMessageVo vo = new AppMessageVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
AppMessageEventEnum event = AppMessageEventEnum.fromCode(entity.getEventCode());
|
||||
vo.setCategory(AppMessageConstants.EVENT_MANUAL_PUSH.equals(entity.getEventCode())
|
||||
? AppMessageConstants.CATEGORY_SYSTEM : event == null ? null : event.getCategory());
|
||||
if (AppMessageConstants.EVENT_MANUAL_PUSH.equals(entity.getEventCode())) {
|
||||
vo.setCategory(AppMessageConstants.CATEGORY_SYSTEM);
|
||||
}
|
||||
else if (AppMessageConstants.EVENT_HOTEL_ROOM_AVAILABLE.equals(entity.getEventCode())) {
|
||||
vo.setCategory(AppMessageConstants.CATEGORY_HOTEL);
|
||||
}
|
||||
else {
|
||||
vo.setCategory(event == null ? null : event.getCategory());
|
||||
}
|
||||
vo.setRead(AppMessageConstants.READ.equals(entity.getReadStatus()));
|
||||
vo.setTitle(resolveLocalizedTitle(entity, event));
|
||||
vo.setContent(resolveLocalizedContent(entity, event));
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.bsd.pjcloud.thirdparty.service.impl.appmessage;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.thirdparty.constant.appmessage.AppMessageConstants;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.entity.appmessage.ThirdAppMessageEntity;
|
||||
import com.bsd.pjcloud.thirdparty.mapper.appmessage.ThirdAppMessageMapper;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdAppMessageSettingService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdHotelRoomNotifyService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdPushDeliveryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 酒店有房通知:仿手动推送旁路,避免进入商户策略矩阵。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ThirdHotelRoomNotifyServiceImpl implements ThirdHotelRoomNotifyService {
|
||||
|
||||
private final ThirdAppMessageMapper messageMapper;
|
||||
|
||||
private final ThirdAppMessageSettingService settingService;
|
||||
|
||||
private final ThirdPushDeliveryService deliveryService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean receive(HotelRoomAvailableEventDto dto) {
|
||||
if (dto == null || StrUtil.isBlank(dto.getEventId()) || dto.getRecipientUserId() == null) {
|
||||
throw new IllegalArgumentException("有房通知参数不完整");
|
||||
}
|
||||
if (ownerMessageExists(dto.getEventId(), dto.getRecipientUserId())) {
|
||||
return true;
|
||||
}
|
||||
String title = trimToMax(dto.getTitle(), AppMessageConstants.MESSAGE_TITLE_MAX_LENGTH);
|
||||
String content = trimToMax(dto.getContent(), AppMessageConstants.MESSAGE_CONTENT_MAX_LENGTH);
|
||||
ThirdAppMessageEntity message = new ThirdAppMessageEntity();
|
||||
message.setTenantId(dto.getTenantId());
|
||||
message.setEventId(dto.getEventId());
|
||||
message.setEventCode(AppMessageConstants.EVENT_HOTEL_ROOM_AVAILABLE);
|
||||
message.setRecipientUserId(dto.getRecipientUserId());
|
||||
message.setSubjectUserId(dto.getRecipientUserId());
|
||||
message.setRecipientType(AppMessageConstants.RECIPIENT_OWNER);
|
||||
message.setSourceService(AppMessageConstants.SOURCE_SUPERMARKET);
|
||||
message.setSourceBizType(AppMessageConstants.EVENT_HOTEL_ROOM_AVAILABLE);
|
||||
message.setSourceBizId(String.valueOf(dto.getHotelId()));
|
||||
message.setSourceMchId(null);
|
||||
message.setSourceMchName(StrUtil.blankToDefault(dto.getSourceName(), null));
|
||||
message.setTitle(title);
|
||||
message.setContent(content);
|
||||
message.setPayloadJson(dto.getPayloadJson());
|
||||
message.setDeepLinkPath(StrUtil.blankToDefault(dto.getDeepLinkPath(), AppMessageConstants.MESSAGE_DETAIL_PATH));
|
||||
message.setDeepLinkParamsJson(dto.getDeepLinkParamsJson());
|
||||
message.setReadStatus(AppMessageConstants.UNREAD);
|
||||
message.setOccurredTime(dto.getOccurredTime());
|
||||
try {
|
||||
if (messageMapper.insert(message) != 1 || message.getId() == null) {
|
||||
throw new IllegalStateException("写入有房通知消息失败");
|
||||
}
|
||||
}
|
||||
catch (DuplicateKeyException conflict) {
|
||||
return true;
|
||||
}
|
||||
String skipReason = settingService.isUserPushEnabled(dto.getRecipientUserId())
|
||||
? null
|
||||
: AppMessageConstants.SKIP_USER_DISABLED;
|
||||
deliveryService.createDelivery(message, skipReason);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean ownerMessageExists(String eventId, Long userId) {
|
||||
Long count = messageMapper.selectCount(Wrappers.<ThirdAppMessageEntity>lambdaQuery()
|
||||
.eq(ThirdAppMessageEntity::getEventId, eventId)
|
||||
.eq(ThirdAppMessageEntity::getRecipientUserId, userId));
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private static String trimToMax(String raw, int max) {
|
||||
String text = StrUtil.nullToEmpty(raw).trim();
|
||||
if (text.length() <= max) {
|
||||
return text;
|
||||
}
|
||||
if (max <= 1) {
|
||||
return text.substring(0, max);
|
||||
}
|
||||
return text.substring(0, max - 1) + "…";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user