This commit is contained in:
zjf committed 2026-11-02 19:59:41 +08:00
1 parent 0c08d59457
commit e6ead96354
12 files changed
+590 -24

No files matched your search

@@ -0,0 +1,60 @@
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.LocalDateTime;
/**
* App 出行/酒店入口可见部门白名单:配置部门及其子部门用户可见,未配置默认全员隐藏。
*/
@Data
@TenantTable
@TableName("sm_hotel_app_entry_dept")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "App出行入口可见部门")
public class SmHotelAppEntryDeptEntity extends Model<SmHotelAppEntryDeptEntity> {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
@Schema(description = "可见部门 ID(含其下子部门)")
private Long deptId;
@Schema(description = "备注")
private String remark;
@Schema(description = "1启用 0停用")
private String status;
@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)
@Schema(description = "部门名称")
private String deptName;
}
@@ -22,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.SmHotelAppEntryService;
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
@@ -124,8 +125,16 @@ public class SmHotelAppController {
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
private final SmHotelAppEntryService appEntryService;
private final StringRedisTemplate stringRedisTemplate;
@Operation(summary = "当前用户是否显示出行Tab/订单出行入口(按部门白名单,默认隐藏)")
@GetMapping("/entry-visible")
public R<Map<String, Object>> entryVisible() {
return R.ok(appEntryService.currentUserEntryVisible());
}
@Operation(summary = "列表筛选项(区域/星级/设施)")
@GetMapping("/filter-meta")
public R<Map<String, Object>> filterMeta() {
@@ -0,0 +1,102 @@
package com.bsd.pjcloud.supermarket.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.supermarket.entity.SmHotelAppEntryDeptEntity;
import com.bsd.pjcloud.supermarket.service.SmHotelAppEntryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 管理端:App 出行 Tab / 订单出行入口按部门显隐。
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/smHotelAppEntry")
@Tag(name = "酒店App出行入口配置")
public class SmHotelAppEntryController {
private final SmHotelAppEntryService appEntryService;
@GetMapping("/page")
@Operation(summary = "分页")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelAppEntry_view')")
public R<Page<SmHotelAppEntryDeptEntity>> page(@ParameterObject Page<SmHotelAppEntryDeptEntity> page,
@ParameterObject SmHotelAppEntryDeptEntity query) {
return R.ok(appEntryService.page(page, query));
}
@GetMapping("/listEnabled")
@Operation(summary = "启用中的可见部门")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelAppEntry_view')")
public R<List<SmHotelAppEntryDeptEntity>> listEnabled() {
return R.ok(appEntryService.listEnabled());
}
@GetMapping("/enabledDeptIds")
@Operation(summary = "启用部门 ID 列表(树勾选回显)")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelAppEntry_view')")
public R<List<Long>> enabledDeptIds() {
return R.ok(appEntryService.listEnabledDeptIds());
}
@PostMapping("/replaceDepts")
@SysLog("配置App出行入口可见部门")
@Operation(summary = "整表替换可见部门(空=全员隐藏)")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelAppEntry_edit')")
public R<Boolean> replaceDepts(@RequestBody Map<String, Object> body) {
@SuppressWarnings("unchecked")
List<Object> raw = body == null ? null : (List<Object>) body.get("deptIds");
List<Long> deptIds = new java.util.ArrayList<>();
if (raw != null) {
for (Object o : raw) {
if (o == null) {
continue;
}
try {
deptIds.add(Long.valueOf(String.valueOf(o)));
}
catch (Exception ignored) {
// skip bad id
}
}
}
String remark = body == null || body.get("remark") == null ? null : String.valueOf(body.get("remark"));
return appEntryService.replaceEnabledDepts(deptIds, remark);
}
@PostMapping
@SysLog("新增App出行入口可见部门")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelAppEntry_edit')")
public R<Boolean> save(@RequestBody SmHotelAppEntryDeptEntity entity) {
return appEntryService.saveOne(entity);
}
@PutMapping
@SysLog("修改App出行入口可见部门")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelAppEntry_edit')")
public R<Boolean> update(@RequestBody SmHotelAppEntryDeptEntity entity) {
return appEntryService.updateOne(entity);
}
@DeleteMapping
@SysLog("删除App出行入口可见部门")
@PreAuthorize("@pms.hasPermission('supermarket_smHotelAppEntry_edit')")
public R<Boolean> remove(@RequestBody List<Long> ids) {
return appEntryService.removeByIds(ids);
}
}
@@ -0,0 +1,9 @@
package com.bsd.pjcloud.supermarket.mapper;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.supermarket.entity.SmHotelAppEntryDeptEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmHotelAppEntryDeptMapper extends PjcloudBaseMapper<SmHotelAppEntryDeptEntity> {
}
@@ -0,0 +1,41 @@
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.SmHotelAppEntryDeptEntity;
import java.util.List;
import java.util.Map;
/**
* App 出行/酒店入口按部门显隐。
*/
public interface SmHotelAppEntryService {
Page<SmHotelAppEntryDeptEntity> page(Page<SmHotelAppEntryDeptEntity> page, SmHotelAppEntryDeptEntity query);
List<SmHotelAppEntryDeptEntity> listEnabled();
/** 当前启用的可见部门 id 列表(管理端树勾选回显) */
List<Long> listEnabledDeptIds();
/**
* 整表替换启用部门:传入的 deptIds 启用,其余软删。
* 空列表 = 全员隐藏。
*/
R<Boolean> replaceEnabledDepts(List<Long> deptIds, String remark);
R<Boolean> saveOne(SmHotelAppEntryDeptEntity entity);
R<Boolean> updateOne(SmHotelAppEntryDeptEntity entity);
R<Boolean> removeByIds(List<Long> ids);
/**
* 当前登录用户是否可见出行 Tab / 订单出行入口。
* 未配置任何启用部门 → false;用户部门命中白名单或其子树 → true。
*/
boolean isTravelVisibleForCurrentUser();
Map<String, Object> currentUserEntryVisible();
}
@@ -24,4 +24,10 @@ public interface SmHotelDownstreamAppService extends IService<SmHotelDownstreamA
/** 订单来源编码:优先 partnerCode,否则 appKey */
String resolvePartnerCode(SmHotelDownstreamAppEntity app);
/**
* 是否为启用中的开放应用(partnerCode 或 appKey 命中)。
* 开放应用走拉模式,不走「渠道对接配置」推送。
*/
boolean isEnabledOpenAppPartner(String partnerCode);
}
@@ -0,0 +1,244 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.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.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.supermarket.entity.SmHotelAppEntryDeptEntity;
import com.bsd.pjcloud.supermarket.mapper.SmHotelAppEntryDeptMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelAppEntryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class SmHotelAppEntryServiceImpl extends ServiceImpl<SmHotelAppEntryDeptMapper, SmHotelAppEntryDeptEntity>
implements SmHotelAppEntryService {
private final RemoteDeptService remoteDeptService;
@Override
public Page<SmHotelAppEntryDeptEntity> page(Page<SmHotelAppEntryDeptEntity> page,
SmHotelAppEntryDeptEntity query) {
Page<SmHotelAppEntryDeptEntity> result = baseMapper.selectPage(page,
Wrappers.<SmHotelAppEntryDeptEntity>lambdaQuery()
.eq(query != null && query.getDeptId() != null, SmHotelAppEntryDeptEntity::getDeptId,
query == null ? null : query.getDeptId())
.eq(query != null && StrUtil.isNotBlank(query.getStatus()), SmHotelAppEntryDeptEntity::getStatus,
query == null ? null : query.getStatus())
.orderByDesc(SmHotelAppEntryDeptEntity::getUpdateTime)
.orderByDesc(SmHotelAppEntryDeptEntity::getCreateTime));
fillDeptNames(result.getRecords());
return result;
}
@Override
public List<SmHotelAppEntryDeptEntity> listEnabled() {
List<SmHotelAppEntryDeptEntity> list = baseMapper.selectList(Wrappers.<SmHotelAppEntryDeptEntity>lambdaQuery()
.eq(SmHotelAppEntryDeptEntity::getStatus, "1")
.orderByAsc(SmHotelAppEntryDeptEntity::getDeptId));
fillDeptNames(list);
return list;
}
@Override
public List<Long> listEnabledDeptIds() {
return listEnabled().stream().map(SmHotelAppEntryDeptEntity::getDeptId).filter(Objects::nonNull).distinct()
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> replaceEnabledDepts(List<Long> deptIds, String remark) {
Set<Long> want = new LinkedHashSet<>();
if (CollUtil.isNotEmpty(deptIds)) {
for (Long id : deptIds) {
if (id != null && id > 0) {
want.add(id);
}
}
}
List<SmHotelAppEntryDeptEntity> existing = baseMapper.selectList(Wrappers.emptyWrapper());
Map<Long, SmHotelAppEntryDeptEntity> byDept = new HashMap<>();
for (SmHotelAppEntryDeptEntity row : existing) {
if (row.getDeptId() != null) {
byDept.put(row.getDeptId(), row);
}
}
// 关掉不在本次勾选里的
for (SmHotelAppEntryDeptEntity row : existing) {
if (row.getDeptId() == null) {
continue;
}
if (!want.contains(row.getDeptId())) {
if (!"0".equals(row.getStatus())) {
row.setStatus("0");
baseMapper.updateById(row);
}
}
}
String rm = StrUtil.trim(remark);
for (Long deptId : want) {
SmHotelAppEntryDeptEntity row = byDept.get(deptId);
if (row == null) {
row = new SmHotelAppEntryDeptEntity();
row.setDeptId(deptId);
row.setStatus("1");
if (StrUtil.isNotBlank(rm)) {
row.setRemark(rm);
}
baseMapper.insert(row);
}
else {
boolean changed = false;
if (!"1".equals(row.getStatus())) {
row.setStatus("1");
changed = true;
}
if (StrUtil.isNotBlank(rm) && !rm.equals(row.getRemark())) {
row.setRemark(rm);
changed = true;
}
if (changed) {
baseMapper.updateById(row);
}
}
}
return R.ok(Boolean.TRUE);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> saveOne(SmHotelAppEntryDeptEntity entity) {
if (entity == null || entity.getDeptId() == null) {
return R.failed("请选择部门");
}
Long cnt = baseMapper.selectCount(Wrappers.<SmHotelAppEntryDeptEntity>lambdaQuery()
.eq(SmHotelAppEntryDeptEntity::getDeptId, entity.getDeptId())
.eq(SmHotelAppEntryDeptEntity::getStatus, "1"));
if (cnt != null && cnt > 0) {
return R.failed("该部门已在可见列表中");
}
SmHotelAppEntryDeptEntity exist = baseMapper.selectOne(Wrappers.<SmHotelAppEntryDeptEntity>lambdaQuery()
.eq(SmHotelAppEntryDeptEntity::getDeptId, entity.getDeptId())
.last("limit 1"));
if (exist != null) {
exist.setStatus("1");
if (StrUtil.isNotBlank(entity.getRemark())) {
exist.setRemark(entity.getRemark());
}
return R.ok(baseMapper.updateById(exist) > 0);
}
if (StrUtil.isBlank(entity.getStatus())) {
entity.setStatus("1");
}
return R.ok(baseMapper.insert(entity) > 0);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> updateOne(SmHotelAppEntryDeptEntity entity) {
if (entity == null || entity.getId() == null) {
return R.failed("缺少主键");
}
return R.ok(baseMapper.updateById(entity) > 0);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Boolean> removeByIds(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return R.failed("请选择要删除的记录");
}
return R.ok(baseMapper.deleteBatchIds(ids) > 0);
}
@Override
public boolean isTravelVisibleForCurrentUser() {
List<Long> whitelist = listEnabledDeptIds();
if (CollUtil.isEmpty(whitelist)) {
return false;
}
PjcloudUser user = SecurityUtils.getUser();
if (user == null || user.getDeptId() == null) {
return false;
}
Long userDeptId = user.getDeptId();
Set<Long> allow = new HashSet<>(whitelist);
if (allow.contains(userDeptId)) {
return true;
}
// 用户部门链(含自身与上级):命中任一白名单部门即可见(白名单部门的子部门用户)
try {
R<List<SysDept>> r = remoteDeptService.getParentDeptList(userDeptId);
if (r != null && r.isOk() && CollUtil.isNotEmpty(r.getData())) {
for (SysDept d : r.getData()) {
if (d != null && d.getDeptId() != null && allow.contains(d.getDeptId())) {
return true;
}
}
}
}
catch (Exception e) {
log.warn("|出行入口|查部门链失败|deptId={}|{}", userDeptId, e.getMessage());
}
return false;
}
@Override
public Map<String, Object> currentUserEntryVisible() {
boolean visible = isTravelVisibleForCurrentUser();
Map<String, Object> data = new LinkedHashMap<>(4);
data.put("travelVisible", visible);
data.put("orderHotelVisible", visible);
return data;
}
private void fillDeptNames(List<SmHotelAppEntryDeptEntity> records) {
if (CollUtil.isEmpty(records)) {
return;
}
List<Long> deptIds = records.stream().map(SmHotelAppEntryDeptEntity::getDeptId).filter(Objects::nonNull)
.distinct().collect(Collectors.toList());
Map<Long, String> nameById = new HashMap<>();
if (!deptIds.isEmpty()) {
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) {
nameById.put(id, d.getName());
}
});
}
}
catch (Exception e) {
log.warn("|出行入口|补全部门名失败|{}", e.getMessage());
}
}
for (SmHotelAppEntryDeptEntity row : records) {
if (row.getDeptId() != null) {
row.setDeptName(nameById.getOrDefault(row.getDeptId(), String.valueOf(row.getDeptId())));
}
}
}
}
@@ -171,6 +171,9 @@ public class SmHotelChannelDeptServiceImpl extends ServiceImpl<SmHotelChannelDep
return partners.stream().filter(StrUtil::isNotBlank).collect(Collectors.toList());
}
/**
* 同一下游伙伴全部启用规则取并集:指定酒店 ∪ 各部门(+区域)命中的上架酒店。
*/
@Override
public List<Long> listHotelIdsForDownstreamPartner(String partnerCode) {
String partner = normalizeCode(partnerCode);
@@ -180,13 +183,15 @@ public class SmHotelChannelDeptServiceImpl extends ServiceImpl<SmHotelChannelDep
List<SmHotelChannelDeptEntity> scopes = baseMapper.selectList(Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
.eq(SmHotelChannelDeptEntity::getDirection, SmHotelChannelDirection.DOWNSTREAM)
.eq(SmHotelChannelDeptEntity::getStatus, "1")
.eq(SmHotelChannelDeptEntity::getChannel, partner));
// 忽略历史大小写差异,避免漏掉同伙伴另一条规则
.apply("lower(channel) = {0}", partner));
if (CollUtil.isEmpty(scopes)) {
return Collections.emptyList();
}
Set<Long> ids = new LinkedHashSet<>();
for (SmHotelChannelDeptEntity scope : scopes) {
String scopeType = SmHotelDownstreamScopeType.normalize(scope.getScopeType());
// 指定酒店:并入名单
if (SmHotelDownstreamScopeType.isHotel(scopeType)) {
if (StrUtil.isBlank(scope.getHotelIds())) {
continue;
@@ -199,6 +204,7 @@ public class SmHotelChannelDeptServiceImpl extends ServiceImpl<SmHotelChannelDep
}
continue;
}
// 按部门(+可选区域):并入该部门上架酒店
if (scope.getDeptId() == null) {
continue;
}
@@ -1,5 +1,6 @@
package com.bsd.pjcloud.supermarket.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
@@ -153,6 +154,24 @@ public class SmHotelDownstreamAppServiceImpl extends ServiceImpl<SmHotelDownstre
return StrUtil.blankToDefault(StrUtil.trim(app.getPartnerCode()), StrUtil.trim(app.getAppKey()));
}
@Override
public boolean isEnabledOpenAppPartner(String partnerCode) {
String code = StrUtil.trim(partnerCode);
if (StrUtil.isBlank(code)) {
return false;
}
List<SmHotelDownstreamAppEntity> apps = baseMapper.selectList(Wrappers.<SmHotelDownstreamAppEntity>lambdaQuery()
.eq(SmHotelDownstreamAppEntity::getStatus, "1")
.select(SmHotelDownstreamAppEntity::getId, SmHotelDownstreamAppEntity::getPartnerCode,
SmHotelDownstreamAppEntity::getAppKey)
.last("limit 500"));
if (CollUtil.isEmpty(apps)) {
return false;
}
return apps.stream().anyMatch(a -> code.equalsIgnoreCase(StrUtil.trim(a.getPartnerCode()))
|| code.equalsIgnoreCase(StrUtil.trim(a.getAppKey())));
}
private boolean existsAppKey(String appKey, Long excludeId) {
Long cnt = baseMapper.selectCount(Wrappers.<SmHotelDownstreamAppEntity>lambdaQuery()
.eq(SmHotelDownstreamAppEntity::getAppKey, appKey)
@@ -265,6 +265,7 @@ public class SmHotelOpenApiServiceImpl implements SmHotelOpenApiService {
row.put("mealType", p.getMealType());
row.put("mealCount", p.getMealCount());
row.put("sellPrice", p.getSellPrice());
row.put("stock", p.getStock() == null ? 0 : p.getStock());
productList.add(row);
}
data.put("products", productList);
@@ -307,9 +308,15 @@ public class SmHotelOpenApiServiceImpl implements SmHotelOpenApiService {
row.put("productCode", d.getProductCode());
row.put("stayDate", d.getStayDate() == null ? null : d.getStayDate().toString());
row.put("sellPrice", d.getSellPrice());
row.put("costPrice", d.getCostPrice());
row.put("stock", d.getStock());
row.put("sold", d.getSold());
row.put("remain", d.getRemainStock());
// remainStock 非表字段,开放接口按 stock-sold 计算,避免下游永远拿不到剩余
int stock = d.getStock() == null ? 0 : d.getStock();
int sold = d.getSold() == null ? 0 : d.getSold();
int remain = Math.max(0, stock - sold);
row.put("remain", remain);
row.put("remainStock", remain);
row.put("dailyStatus", d.getDailyStatus());
list.add(row);
}
@@ -1843,7 +1843,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
int need = rooms < 1 ? 1 : rooms;
List<LocalDate> missing = new ArrayList<>();
List<LocalDate> closed = new ArrayList<>();
List<String> shortStock = new ArrayList<>();
List<LocalDate> shortStock = new ArrayList<>();
for (LocalDate d : SmHotelStayDates.occupyDates(checkIn, checkOut)) {
SmHotelDailyEntity row = dailyMapper.selectOne(Wrappers.<SmHotelDailyEntity>lambdaQuery()
.eq(SmHotelDailyEntity::getProductId, productId)
@@ -1860,29 +1860,40 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
int stock = row.getStock() == null ? 0 : row.getStock();
int sold = row.getSold() == null ? 0 : row.getSold();
if (stock - sold < need) {
shortStock.add(d + " 可售" + Math.max(0, stock - sold));
shortStock.add(d);
}
}
if (missing.isEmpty() && closed.isEmpty() && shortStock.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder("所选入住区间不可订");
if (!missing.isEmpty()) {
sb.append(":缺少日历 ").append(formatDateList(missing));
// C 端可读文案:避免「可售0 / 缺少日历」等运营术语
StringBuilder sb = new StringBuilder();
if (!shortStock.isEmpty() || !closed.isEmpty()) {
sb.append("你选择的日期房间已订满或暂不可订");
List<String> badDays = new ArrayList<>();
for (LocalDate d : closed) {
badDays.add(formatStayDayCn(d));
}
for (LocalDate d : shortStock) {
badDays.add(formatStayDayCn(d));
}
if (!badDays.isEmpty()) {
int n = Math.min(badDays.size(), 5);
sb.append("(").append(String.join("、", badDays.subList(0, n))).append(")");
}
}
if (!closed.isEmpty()) {
sb.append(missing.isEmpty() ? ":" : ";").append("已关房 ").append(formatDateList(closed));
else if (!missing.isEmpty()) {
sb.append("你选择的日期暂无房价,请更换日期");
}
if (!shortStock.isEmpty()) {
sb.append(missing.isEmpty() && closed.isEmpty() ? ":" : ";")
.append("库存不足 ").append(String.join("、", shortStock));
else {
sb.append("当前日期暂不可预订");
}
String hint = buildAvailableDatesHint(productId, need);
if (StrUtil.isNotBlank(hint)) {
sb.append("。").append(hint);
}
else {
sb.append("。请更换入住日期或联系商家维护房价房量");
sb.append("。请更换入住日期后再试");
}
return sb.toString();
}
@@ -1913,9 +1924,52 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
}
}
if (ok.isEmpty()) {
return "该房型近60天暂无可住日期";
return "近两月暂无空房,可稍后再看或换其他房型";
}
return "近期可住:" + formatDateRanges(ok);
return "建议改订:" + formatDateRangesCn(ok);
}
private static String formatStayDayCn(LocalDate d) {
if (d == null) {
return "";
}
return d.getMonthValue() + "月" + d.getDayOfMonth() + "日";
}
/** 近期可住区间,中文月日展示,如 11月3日至11日、11月15日 */
private static String formatDateRangesCn(List<LocalDate> dates) {
if (dates == null || dates.isEmpty()) {
return "";
}
List<String> parts = new ArrayList<>();
LocalDate start = dates.get(0);
LocalDate prev = start;
for (int i = 1; i < dates.size(); i++) {
LocalDate cur = dates.get(i);
if (prev.plusDays(1).equals(cur)) {
prev = cur;
continue;
}
parts.add(formatRangePartCn(start, prev));
if (parts.size() >= 4) {
parts.add("等");
return String.join("、", parts);
}
start = cur;
prev = cur;
}
parts.add(formatRangePartCn(start, prev));
return String.join("、", parts);
}
private static String formatRangePartCn(LocalDate start, LocalDate end) {
if (start.equals(end)) {
return formatStayDayCn(start);
}
if (start.getYear() == end.getYear() && start.getMonth() == end.getMonth()) {
return formatStayDayCn(start) + "至" + end.getDayOfMonth() + "日";
}
return formatStayDayCn(start) + "至" + formatStayDayCn(end);
}
private static String formatDateList(List<LocalDate> dates) {
@@ -2331,15 +2385,16 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
for (LocalDate d : SmHotelStayDates.occupyDates(checkIn, checkOut)) {
int updated = dailyMapper.increaseSoldIfEnough(productId, d, rooms);
if (updated == 0) {
SmHotelDailyEntity row = dailyMapper.selectOne(Wrappers.<SmHotelDailyEntity>lambdaQuery()
.eq(SmHotelDailyEntity::getProductId, productId)
.eq(SmHotelDailyEntity::getStayDate, d)
.last("limit 1"));
int stock = row == null || row.getStock() == null ? 0 : row.getStock();
int sold = row == null || row.getSold() == null ? 0 : row.getSold();
String hint = buildAvailableDatesHint(productId, rooms);
return "所选入住区间不可订:日期 " + d + " 可售 " + Math.max(0, stock - sold) + ",需要 " + rooms
+ (StrUtil.isNotBlank(hint) ? "。" + hint : "");
StringBuilder race = new StringBuilder("你选择的日期房间刚被订完(")
.append(formatStayDayCn(d)).append(")");
if (StrUtil.isNotBlank(hint)) {
race.append("。").append(hint);
}
else {
race.append("。请更换入住日期后再试");
}
return race.toString();
}
}
// 占房成功:下游(环启)需同步剩余可售,避免仍按原配额售卖
@@ -21,6 +21,7 @@ import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
import com.bsd.pjcloud.supermarket.mapper.SmHotelSyncLogMapper;
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAppService;
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamPushLogService;
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
import com.bsd.pjcloud.supermarket.service.SmHotelSyncService;
@@ -104,6 +105,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
private final SmHotelChannelDeptService channelDeptService;
private final SmHotelDownstreamAppService downstreamAppService;
private final SmHotelMapper hotelMapper;
private final SmHotelSyncLogMapper syncLogMapper;
@@ -289,7 +292,12 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
return R.failed("请选择下游伙伴");
}
if (!channelConfigService.listEnabledDownstreamPartners().contains(partner)) {
return R.failed("下游伙伴「" + partner + "」未启用或不存在,请先到「渠道对接配置」维护");
// 开放应用是对方主动拉数,不走协议推送;误点「全量推送」时给出明确说明
if (downstreamAppService.isEnabledOpenAppPartner(partner)) {
return R.failed("「" + partner + "」是开放应用(拉模式),无需也不支持「全量推送」。"
+ "请在「渠道范围映射」配好授权即可,对方用 appKey 调开放 API 自行拉酒店/下单");
}
return R.failed("下游伙伴「" + partner + "」未启用或不存在,请先到「渠道对接配置」维护(圣诺/慧订/环启等协议下游)");
}
Long running = syncLogMapper.selectCount(Wrappers.<SmHotelSyncLogEntity>lambdaQuery()
.eq(SmHotelSyncLogEntity::getSyncType, TYPE_DOWNSTREAM_PUSH)