This commit is contained in:
zjf committed 2026-10-23 12:31:35 +08:00
1 parent 2db14465a9
commit aadf00bc92
5 files changed
+156 -66

No files matched your search

@@ -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;
}
@@ -84,6 +84,9 @@ public class SmHotelOrderDetailVo {
private BigDecimal latitude;
private Long deptId;
@Schema(description = "酒店归属部门名称(商户)")
private String deptName;
}
@Data
@@ -41,7 +41,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 +90,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;
@@ -425,16 +424,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 +464,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 +472,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;
}
@@ -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 &lt;&gt; 'OFFLINE')
AND (hotel_name LIKE CONCAT(#{keyword}, '%') OR hotel_code = #{keyword})
<if test="adFrom != null">
AND district_adcode &gt;= #{adFrom} AND district_adcode &lt; #{adTo}
</if>
<if test="starLevels != null and starLevels.size() &gt; 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 &lt;&gt; 'OFFLINE')
AND hotel_name LIKE CONCAT('%', #{keyword}, '%')
<if test="adFrom != null">
AND district_adcode &gt;= #{adFrom} AND district_adcode &lt; #{adTo}
</if>
<if test="starLevels != null and starLevels.size() &gt; 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);
}
@@ -26,6 +26,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 +110,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
private final SmHotelRoomMapper roomMapper;
private final RemoteDeptService remoteDeptService;
@Override
public Page<SmHotelOrderEntity> pageOrders(Page<SmHotelOrderEntity> page, SmHotelOrderEntity query) {
Page<SmHotelOrderEntity> result = baseMapper.selectPage(page, Wrappers.<SmHotelOrderEntity>lambdaQuery()
@@ -343,6 +347,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 +385,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 +560,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;