diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java index cf676bb5..415bbad7 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelOrderEntity.java @@ -108,4 +108,19 @@ public class SmHotelOrderEntity extends Model { @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; } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java index d10165df..f3471682 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java @@ -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> filterMeta() { var online = Wrappers.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 starList = parseIntCsv(starLevels); List facilityList = parseStrCsv(facilityTags); var wrapper = Wrappers.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 createOrder(@RequestBody Map body) { PjcloudUser user = SecurityUtils.getUser(); - if (user == null) { + if (user == null || user.getId() == null) { return R.failed("请先登录"); } Map 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 raw = orderService.pageOrders(page, query); + Page raw = orderService.pageMyAppOrders(page, user, StrUtil.trimToNull(orderStatus)); List hotelIds = raw.getRecords().stream() .map(SmHotelOrderEntity::getHotelId) .filter(Objects::nonNull) .distinct() .toList(); Map hotelNameMap = new HashMap<>(); + Map hotelCoverMap = new HashMap<>(); if (!hotelIds.isEmpty()) { List hotels = hotelService.listByIds(hotelIds); for (SmHotelEntity h : hotels) { hotelNameMap.put(h.getId(), h.getHotelName()); + hotelCoverMap.put(h.getId(), h.getCoverImage()); } } + // 补齐房型名 + Map 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> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal()); out.setRecords(raw.getRecords().stream().map(o -> { Map 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 orderDetail(@PathVariable Long id) { + public R> 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 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", "用户取消")); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java index 95013096..0a81bdc4 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java @@ -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 pageOrders(Page page, SmHotelOrderEntity query); + /** App「我的订单」:按用户 ID 绑定(channelNo 前缀 APP{userId}) */ + Page pageMyAppOrders(Page page, PjcloudUser user, String orderStatus); + + /** App 订单归属:仅认 APP{userId},不依赖用户名(改名不影响) */ + boolean isOwnedByAppUser(SmHotelOrderEntity order, PjcloudUser user); + SmHotelOrderEntity getDetail(Long id); /** 下游回传下单 → 可定校验 → 上游创建 → 必要时挂补偿 */ diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java index 5db53867..eeaa1436 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java @@ -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 pageOrders(Page page, SmHotelOrderEntity query) { - return baseMapper.selectPage(page, Wrappers.lambdaQuery() + Page result = baseMapper.selectPage(page, Wrappers.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 pageMyAppOrders(Page 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 result = baseMapper.selectPage(page, Wrappers.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 records) { + if (records == null || records.isEmpty()) { + return; + } + List hotelIds = records.stream() + .map(SmHotelOrderEntity::getHotelId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + Map hotelById = new HashMap<>(); + if (!hotelIds.isEmpty()) { + for (SmHotelEntity h : hotelMapper.selectByIds(hotelIds)) { + hotelById.put(h.getId(), h); + } + } + // hotelId 缺失时按 hotelCode 补名(历史试下单/上游单) + List codes = records.stream() + .filter(o -> o.getHotelId() == null || !hotelById.containsKey(o.getHotelId())) + .map(SmHotelOrderEntity::getHotelCode) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + Map hotelByCode = new HashMap<>(); + if (!codes.isEmpty()) { + List byCode = hotelMapper.selectList(Wrappers.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 ServiceImpllambdaQuery() + .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,成功则本地确认,不调上游。 */