This commit is contained in:
zjf committed 2026-10-12 14:17:22 +08:00
1 parent e3e2726f91
commit b4e83d5604
4 files changed
+247 -13

No files matched your search

@@ -108,4 +108,19 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
/** 列表展示:酒店名称(非表字段) */
@TableField(exist = false)
@Schema(description = "酒店名称")
private String hotelName;
/** 列表展示:下单来源 APP/ADMIN/DOWNSTREAM(非表字段,由 channelNo 推断) */
@TableField(exist = false)
@Schema(description = "下单来源")
private String orderSource;
/** 列表展示:酒店数据来源 local/shangyou(非表字段) */
@TableField(exist = false)
@Schema(description = "酒店数据来源")
private String hotelDataSource;
}
@@ -10,6 +10,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.constant.SmHotelDataSource;
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
@@ -62,6 +63,7 @@ public class SmHotelAppController {
@GetMapping("/filter-meta")
public R<Map<String, Object>> filterMeta() {
var online = Wrappers.<SmHotelEntity>lambdaQuery()
.eq(SmHotelEntity::getSheetName, SmHotelDataSource.LOCAL)
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
.or()
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
@@ -129,6 +131,8 @@ public class SmHotelAppController {
List<Integer> starList = parseIntCsv(starLevels);
List<String> facilityList = parseStrCsv(facilityTags);
var wrapper = Wrappers.<SmHotelEntity>lambdaQuery()
// 移动端 TOC 仅展示本地酒店,不展示上游同步酒店
.eq(SmHotelEntity::getSheetName, SmHotelDataSource.LOCAL)
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
.or()
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
@@ -187,7 +191,11 @@ public class SmHotelAppController {
if (r.getCode() != 0 || r.getData() == null || r.getData().getHotel() == null) {
return r;
}
if ("OFFLINE".equalsIgnoreCase(r.getData().getHotel().getUpstreamStatus())) {
SmHotelEntity hotel = r.getData().getHotel();
if (!SmHotelDataSource.isLocal(hotel.getSheetName())) {
return R.failed("仅支持预订本地酒店");
}
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
return R.failed("酒店已下架");
}
return r;
@@ -202,6 +210,9 @@ public class SmHotelAppController {
if (hotel == null) {
return R.failed("酒店不存在");
}
if (!SmHotelDataSource.isLocal(hotel.getSheetName())) {
return R.failed("仅支持预订本地酒店");
}
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
return R.failed("酒店已下架");
}
@@ -212,13 +223,26 @@ public class SmHotelAppController {
@PostMapping("/order/create")
public R<JSONObject> createOrder(@RequestBody Map<String, Object> body) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null) {
if (user == null || user.getId() == null) {
return R.failed("请先登录");
}
Map<String, Object> req = body == null ? new HashMap<>() : new HashMap<>(body);
if (StrUtil.isBlank(str(req.get("channelNo")))) {
req.put("channelNo", "APP" + user.getId() + System.currentTimeMillis());
Long hotelId = null;
Object hotelIdObj = req.get("hotelId");
if (hotelIdObj != null && StrUtil.isNotBlank(String.valueOf(hotelIdObj))) {
hotelId = Long.valueOf(String.valueOf(hotelIdObj));
}
if (hotelId != null) {
SmHotelEntity hotel = hotelService.getById(hotelId);
if (hotel == null) {
return R.failed("酒店不存在");
}
if (!SmHotelDataSource.isLocal(hotel.getSheetName())) {
return R.failed("仅支持预订本地酒店");
}
}
// 渠道单号绑定用户 ID:APP{userId}{ts},列表/详情/取消均按此前缀认主(与用户名无关)
req.put("channelNo", "APP" + user.getId() + System.currentTimeMillis());
if (StrUtil.isBlank(str(req.get("contactName")))) {
req.put("contactName", StrUtil.blankToDefault(user.getName(), user.getUsername()));
}
@@ -236,22 +260,38 @@ public class SmHotelAppController {
if (user == null) {
return R.failed("请先登录");
}
SmHotelOrderEntity query = new SmHotelOrderEntity();
query.setOrderStatus(StrUtil.trimToNull(orderStatus));
query.setCreateBy(user.getUsername());
Page<SmHotelOrderEntity> raw = orderService.pageOrders(page, query);
Page<SmHotelOrderEntity> raw = orderService.pageMyAppOrders(page, user, StrUtil.trimToNull(orderStatus));
List<Long> hotelIds = raw.getRecords().stream()
.map(SmHotelOrderEntity::getHotelId)
.filter(Objects::nonNull)
.distinct()
.toList();
Map<Long, String> hotelNameMap = new HashMap<>();
Map<Long, String> hotelCoverMap = new HashMap<>();
if (!hotelIds.isEmpty()) {
List<SmHotelEntity> hotels = hotelService.listByIds(hotelIds);
for (SmHotelEntity h : hotels) {
hotelNameMap.put(h.getId(), h.getHotelName());
hotelCoverMap.put(h.getId(), h.getCoverImage());
}
}
// 补齐房型名
Map<String, String> productNameMap = new HashMap<>();
for (SmHotelOrderEntity o : raw.getRecords()) {
if (o.getHotelId() == null || StrUtil.isBlank(o.getProductCode())) {
continue;
}
String key = o.getHotelId() + "#" + o.getProductCode();
if (productNameMap.containsKey(key)) {
continue;
}
SmHotelProductEntity p = Db.lambdaQuery(SmHotelProductEntity.class)
.eq(SmHotelProductEntity::getHotelId, o.getHotelId())
.eq(SmHotelProductEntity::getProductCode, o.getProductCode())
.last("limit 1")
.one();
productNameMap.put(key, p == null ? null : p.getProductName());
}
Page<Map<String, Object>> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal());
out.setRecords(raw.getRecords().stream().map(o -> {
Map<String, Object> row = new LinkedHashMap<>();
@@ -261,7 +301,9 @@ public class SmHotelAppController {
row.put("hotelId", o.getHotelId());
row.put("hotelCode", o.getHotelCode());
row.put("hotelName", hotelNameMap.get(o.getHotelId()));
row.put("coverImage", hotelCoverMap.get(o.getHotelId()));
row.put("productCode", o.getProductCode());
row.put("productName", productNameMap.get(o.getHotelId() + "#" + o.getProductCode()));
row.put("roomTypeCode", o.getRoomTypeCode());
row.put("checkInDate", o.getCheckInDate());
row.put("checkOutDate", o.getCheckOutDate());
@@ -279,7 +321,7 @@ public class SmHotelAppController {
@Operation(summary = "我的订单详情")
@GetMapping("/order/{id}")
public R<SmHotelOrderEntity> orderDetail(@PathVariable Long id) {
public R<Map<String, Object>> orderDetail(@PathVariable Long id) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null) {
return R.failed("请先登录");
@@ -288,10 +330,47 @@ public class SmHotelAppController {
if (order == null) {
return R.failed("订单不存在");
}
if (!user.getUsername().equals(order.getCreateBy())) {
if (!orderService.isOwnedByAppUser(order, user)) {
return R.failed("无权查看该订单");
}
return R.ok(order);
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", order.getId());
row.put("channelNo", order.getChannelNo());
row.put("platformNo", order.getPlatformNo());
row.put("hotelId", order.getHotelId());
row.put("hotelCode", order.getHotelCode());
row.put("productCode", order.getProductCode());
row.put("roomTypeCode", order.getRoomTypeCode());
row.put("checkInDate", order.getCheckInDate());
row.put("checkOutDate", order.getCheckOutDate());
row.put("count", order.getCount());
row.put("sellAmount", order.getSellAmount());
row.put("orderStatus", order.getOrderStatus());
row.put("bookStatus", order.getBookStatus());
row.put("contactName", order.getContactName());
row.put("contactPhone", order.getContactPhone());
row.put("createTime", order.getCreateTime());
row.put("failMsg", order.getFailMsg());
if (order.getHotelId() != null) {
SmHotelEntity hotel = hotelService.getById(order.getHotelId());
if (hotel != null) {
row.put("hotelName", hotel.getHotelName());
row.put("coverImage", hotel.getCoverImage());
row.put("address", hotel.getAddress());
row.put("phone", hotel.getPhone());
}
if (StrUtil.isNotBlank(order.getProductCode())) {
SmHotelProductEntity p = Db.lambdaQuery(SmHotelProductEntity.class)
.eq(SmHotelProductEntity::getHotelId, order.getHotelId())
.eq(SmHotelProductEntity::getProductCode, order.getProductCode())
.last("limit 1")
.one();
if (p != null) {
row.put("productName", p.getProductName());
}
}
}
return R.ok(row);
}
@Operation(summary = "取消我的订单")
@@ -305,7 +384,7 @@ public class SmHotelAppController {
if (order == null) {
return R.failed("订单不存在");
}
if (!user.getUsername().equals(order.getCreateBy())) {
if (!orderService.isOwnedByAppUser(order, user)) {
return R.failed("无权取消该订单");
}
String reason = body == null ? "用户取消" : String.valueOf(body.getOrDefault("reason", "用户取消"));
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.supermarket.service;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
import com.bsd.pjcloud.common.core.util.R;
@@ -11,6 +12,12 @@ public interface SmHotelOrderService {
Page<SmHotelOrderEntity> pageOrders(Page<SmHotelOrderEntity> page, SmHotelOrderEntity query);
/** App「我的订单」:按用户 ID 绑定(channelNo 前缀 APP{userId}) */
Page<SmHotelOrderEntity> pageMyAppOrders(Page<SmHotelOrderEntity> page, PjcloudUser user, String orderStatus);
/** App 订单归属:仅认 APP{userId},不依赖用户名(改名不影响) */
boolean isOwnedByAppUser(SmHotelOrderEntity order, PjcloudUser user);
SmHotelOrderEntity getDetail(Long id);
/** 下游回传下单 → 可定校验 → 上游创建 → 必要时挂补偿 */
@@ -19,6 +19,7 @@ import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
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.common.security.service.PjcloudUser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -28,8 +29,11 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 中转订单:可定校验 → 上游下单 → 不确定/3028/P 状态补偿(文档 3/5/10 分钟)。
@@ -67,7 +71,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
@Override
public Page<SmHotelOrderEntity> pageOrders(Page<SmHotelOrderEntity> page, SmHotelOrderEntity query) {
return baseMapper.selectPage(page, Wrappers.<SmHotelOrderEntity>lambdaQuery()
Page<SmHotelOrderEntity> result = baseMapper.selectPage(page, Wrappers.<SmHotelOrderEntity>lambdaQuery()
.like(StrUtil.isNotBlank(query.getChannelNo()), SmHotelOrderEntity::getChannelNo, query.getChannelNo())
.like(StrUtil.isNotBlank(query.getPlatformNo()), SmHotelOrderEntity::getPlatformNo,
query.getPlatformNo())
@@ -77,8 +81,99 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
.eq(StrUtil.isNotBlank(query.getCompensateStatus()), SmHotelOrderEntity::getCompensateStatus,
query.getCompensateStatus())
.eq(StrUtil.isNotBlank(query.getHotelCode()), SmHotelOrderEntity::getHotelCode, query.getHotelCode())
.like(StrUtil.isNotBlank(query.getContactName()), SmHotelOrderEntity::getContactName,
query.getContactName())
.like(StrUtil.isNotBlank(query.getContactPhone()), SmHotelOrderEntity::getContactPhone,
query.getContactPhone())
.eq(StrUtil.isNotBlank(query.getCreateBy()), SmHotelOrderEntity::getCreateBy, query.getCreateBy())
.orderByDesc(SmHotelOrderEntity::getCreateTime));
// 后台列表补齐酒店名与来源,便于识别 App 下单
fillOrderListDisplay(result.getRecords());
return result;
}
@Override
public Page<SmHotelOrderEntity> pageMyAppOrders(Page<SmHotelOrderEntity> page, PjcloudUser user,
String orderStatus) {
if (user == null || user.getId() == null) {
return page;
}
// 归属绑定用户 ID:channelNo = APP{userId}{ts};不按 createBy/用户名/手机号(改名、撞号都不串单)
String appChannelPrefix = "APP" + user.getId();
Page<SmHotelOrderEntity> result = baseMapper.selectPage(page, Wrappers.<SmHotelOrderEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(orderStatus), SmHotelOrderEntity::getOrderStatus, orderStatus)
.likeRight(SmHotelOrderEntity::getChannelNo, appChannelPrefix)
.orderByDesc(SmHotelOrderEntity::getCreateTime));
fillOrderListDisplay(result.getRecords());
return result;
}
@Override
public boolean isOwnedByAppUser(SmHotelOrderEntity order, PjcloudUser user) {
if (order == null || user == null || user.getId() == null) {
return false;
}
return StrUtil.startWith(order.getChannelNo(), "APP" + user.getId());
}
/**
* 分页列表展示字段:酒店名称、数据来源(本地/上游)、channelNo 推断下单来源。
*/
private void fillOrderListDisplay(List<SmHotelOrderEntity> records) {
if (records == null || records.isEmpty()) {
return;
}
List<Long> hotelIds = records.stream()
.map(SmHotelOrderEntity::getHotelId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Long, SmHotelEntity> hotelById = new HashMap<>();
if (!hotelIds.isEmpty()) {
for (SmHotelEntity h : hotelMapper.selectByIds(hotelIds)) {
hotelById.put(h.getId(), h);
}
}
// hotelId 缺失时按 hotelCode 补名(历史试下单/上游单)
List<String> codes = records.stream()
.filter(o -> o.getHotelId() == null || !hotelById.containsKey(o.getHotelId()))
.map(SmHotelOrderEntity::getHotelCode)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
Map<String, SmHotelEntity> hotelByCode = new HashMap<>();
if (!codes.isEmpty()) {
List<SmHotelEntity> byCode = hotelMapper.selectList(Wrappers.<SmHotelEntity>lambdaQuery()
.in(SmHotelEntity::getHotelCode, codes));
for (SmHotelEntity h : byCode) {
if (StrUtil.isNotBlank(h.getHotelCode())) {
hotelByCode.putIfAbsent(h.getHotelCode(), h);
}
}
}
for (SmHotelOrderEntity o : records) {
SmHotelEntity hotel = o.getHotelId() != null ? hotelById.get(o.getHotelId()) : null;
if (hotel == null && StrUtil.isNotBlank(o.getHotelCode())) {
hotel = hotelByCode.get(o.getHotelCode());
}
if (hotel != null) {
o.setHotelName(hotel.getHotelName());
o.setHotelDataSource(SmHotelDataSource.isLocal(hotel.getSheetName()) ? "local" : "shangyou");
}
String channelNo = o.getChannelNo();
if (StrUtil.startWithIgnoreCase(channelNo, "APP")) {
o.setOrderSource("APP");
}
else if (StrUtil.startWithIgnoreCase(channelNo, "ADMIN")) {
o.setOrderSource("ADMIN");
}
else if (StrUtil.startWithIgnoreCase(o.getPlatformNo(), "LOCAL")) {
o.setOrderSource("LOCAL");
}
else {
o.setOrderSource(StrUtil.isBlank(channelNo) ? "-" : "DOWNSTREAM");
}
}
}
@Override
@@ -316,6 +411,14 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
order.setInventoryStatus(SmHotelInventoryStatus.NONE);
order.setCompensateRound(0);
order.setReqJson(JSONUtil.toJsonStr(body));
// 本地酒店:先校验日历库存,避免 insert 后因缺库存整单回滚,App 假成功/后台无单
if (localHotel) {
String precheck = precheckLocalInventory(product.getId(), checkIn, checkOut, count);
if (precheck != null) {
return R.failed(precheck);
}
}
baseMapper.insert(order);
if (localHotel) {
@@ -354,6 +457,36 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
return R.ok(out, "下单已提交");
}
/**
* 本地下单前只读校验日历库存(不占用 sold)。
*
* @return null=可下;非空=失败原因
*/
private String precheckLocalInventory(Long productId, LocalDate checkIn, LocalDate checkOut, int rooms) {
if (productId == null || checkIn == null || checkOut == null || !checkIn.isBefore(checkOut)) {
return "入住/离店日期无效";
}
int need = rooms < 1 ? 1 : rooms;
for (LocalDate d = checkIn; d.isBefore(checkOut); d = d.plusDays(1)) {
SmHotelDailyEntity row = dailyMapper.selectOne(Wrappers.<SmHotelDailyEntity>lambdaQuery()
.eq(SmHotelDailyEntity::getProductId, productId)
.eq(SmHotelDailyEntity::getStayDate, d)
.last("limit 1"));
if (row == null) {
return "本地库存不足:缺少日期 " + d + " 的日历,请先在后台维护本地房价房量";
}
if (!Integer.valueOf(1).equals(row.getDailyStatus())) {
return "本地库存不足:日期 " + d + " 已关房";
}
int stock = row.getStock() == null ? 0 : row.getStock();
int sold = row.getSold() == null ? 0 : row.getSold();
if (stock - sold < need) {
return "本地库存不足:日期 " + d + " 可售 " + Math.max(0, stock - sold) + ",需要 " + need;
}
}
return null;
}
/**
* 本地酒店下单:按入住夜 CAS 增加 sold,成功则本地确认,不调上游。
*/