1.酒店 2.商户增加酒店类型,部门接口适配酒店

This commit is contained in:
zjf committed 2026-10-12 19:51:36 +08:00
1 parent 074340e5c6
commit 8ef2f8ccd6
15 files changed
+593 -47

No files matched your search

@@ -16,6 +16,8 @@ public enum MerchantTypeEnum {
SELF_SUPERMARKET("3", "自营商家"),
THIRD_APP("4", "第三方应用"),
FARMER("5", "共富商城(助农)"),
/** 酒店预订收款商户,与食堂/商超隔离 */
HOTEL("6", "酒店"),
;
/**
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.supermarket.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.supermarket.vo.SmHotelOrderPayInfoVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 酒店订单内部 Feign:支付发起校验与支付成功履约。
*/
@FeignClient(contextId = "remoteSmHotelOrderService", value = ServiceNameConstants.SUPERMARKET_SERVICE)
public interface RemoteSmHotelOrderService {
/**
* 读取待支付酒店订单的可信金额与归属用户。
*/
@NoToken
@GetMapping("/inner/sm-hotel-order/pay-info")
R<SmHotelOrderPayInfoVo> getPayInfo(@RequestParam("orderId") Long orderId);
/**
* 第三方支付成功后履约:标记已付;本地酒店扣库存确认,上游酒店再调上游下单。
*/
@NoToken
@PostMapping("/inner/sm-hotel-order/pay-success")
R<Boolean> paySuccess(@RequestParam("orderId") Long orderId);
}
@@ -0,0 +1,33 @@
package com.bsd.pjcloud.supermarket.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 酒店订单支付可信信息,供 thirdparty 创建支付单时校验金额与归属。
*/
@Data
@Schema(description = "酒店订单支付信息")
public class SmHotelOrderPayInfoVo {
@Schema(description = "酒店订单主键")
private Long orderId;
@Schema(description = "下单 App 用户 ID(从 channelNo 解析)")
private Long userId;
@Schema(description = "酒店所属部门(收款商户按此解析,非下单用户部门)")
private Long hotelDeptId;
@Schema(description = "应付金额(分)")
private Integer amountFen;
@Schema(description = "本地支付状态 UNPAID/PAID")
private String payStatus;
@Schema(description = "订单状态")
private String orderStatus;
@Schema(description = "是否允许发起支付")
private Boolean payable;
}
@@ -10,7 +10,6 @@ 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;
@@ -63,7 +62,6 @@ 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"))
@@ -131,8 +129,6 @@ 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"))
@@ -191,11 +187,7 @@ public class SmHotelAppController {
if (r.getCode() != 0 || r.getData() == null || r.getData().getHotel() == null) {
return r;
}
SmHotelEntity hotel = r.getData().getHotel();
if (!SmHotelDataSource.isLocal(hotel.getSheetName())) {
return R.failed("仅支持预订本地酒店");
}
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
if ("OFFLINE".equalsIgnoreCase(r.getData().getHotel().getUpstreamStatus())) {
return R.failed("酒店已下架");
}
return r;
@@ -210,9 +202,6 @@ 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("酒店已下架");
}
@@ -227,20 +216,6 @@ public class SmHotelAppController {
return R.failed("请先登录");
}
Map<String, Object> req = body == null ? new HashMap<>() : new HashMap<>(body);
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")))) {
@@ -249,7 +224,8 @@ public class SmHotelAppController {
if (StrUtil.isBlank(str(req.get("contactPhone")))) {
req.put("contactPhone", user.getPhone());
}
return orderService.createByAdmin(req);
// App 下单先落待支付订单,支付成功后再履约(本地扣库存 / 上游下单)
return orderService.createAppPendingOrder(req);
}
@Operation(summary = "我的酒店订单分页")
@@ -0,0 +1,40 @@
package com.bsd.pjcloud.supermarket.controller;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 酒店订单内部接口:供 thirdparty 支付校验与支付成功回调。
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/inner/sm-hotel-order")
@Tag(name = "酒店订单内部接口")
public class SmHotelOrderInnerController {
private final SmHotelOrderService orderService;
@Inner
@Operation(summary = "支付前读取订单可信金额")
@GetMapping("/pay-info")
public R<SmHotelOrderPayInfoVo> payInfo(@RequestParam("orderId") Long orderId) {
return R.ok(orderService.getPayInfo(orderId));
}
@Inner
@Operation(summary = "支付成功履约")
@PostMapping("/pay-success")
public R<Boolean> paySuccess(@RequestParam("orderId") Long orderId) {
return R.ok(orderService.onAppPaySuccess(orderId));
}
}
@@ -4,6 +4,7 @@ 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.supermarket.vo.SmHotelOrderPayInfoVo;
import com.bsd.pjcloud.common.core.util.R;
import java.util.Map;
@@ -29,6 +30,19 @@ public interface SmHotelOrderService {
*/
R<JSONObject> createByAdmin(Map<String, Object> body);
/**
* App 待支付创单:只落业务订单,不扣本地库存、不调上游;支付成功后再履约。
*/
R<JSONObject> createAppPendingOrder(Map<String, Object> body);
/** 供 thirdparty 拉起支付前校验 */
SmHotelOrderPayInfoVo getPayInfo(Long orderId);
/**
* App 支付成功履约。本地酒店只确认库存;上游酒店再调上游下单。
*/
Boolean onAppPaySuccess(Long orderId);
/** 下游回传取消 → 调上游取消 */
R<JSONObject> cancelFromDownstream(String appKey, Map<String, Object> body);
@@ -16,6 +16,7 @@ import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouOrderCallResult;
import com.bsd.pjcloud.supermarket.mapper.*;
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
@@ -26,6 +27,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -55,6 +57,12 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
private static final String COMP_NEED_MANUAL = "NEED_MANUAL";
/** App 本地支付状态:待支付(尚未拉起微信/支付宝成功回调) */
private static final String PAY_UNPAID = "UNPAID";
/** App 本地支付状态:已支付 */
private static final String PAY_PAID = "PAID";
private final SmHotelDownstreamAppMapper appMapper;
private final SmHotelMapper hotelMapper;
@@ -457,6 +465,316 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
return R.ok(out, "下单已提交");
}
/**
* App 待支付创单:校验与金额落库后返回,支付成功再履约。
* 本地酒店仅预检库存不占用;上游酒店此时不调上游。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<JSONObject> createAppPendingOrder(Map<String, Object> body) {
if (body == null) {
return R.failed("请求体不能为空");
}
Long hotelId = null;
Object hotelIdObj = body.get("hotelId");
if (hotelIdObj != null && StrUtil.isNotBlank(String.valueOf(hotelIdObj))) {
hotelId = Long.valueOf(String.valueOf(hotelIdObj));
}
SmHotelEntity hotel = null;
if (hotelId != null) {
hotel = hotelMapper.selectById(hotelId);
}
if (hotel == null) {
String hotelCode = str(body.get("hotelCode"));
if (StrUtil.isNotBlank(hotelCode)) {
hotel = hotelMapper.selectOne(Wrappers.<SmHotelEntity>lambdaQuery()
.eq(SmHotelEntity::getHotelCode, hotelCode)
.last("limit 1"));
}
}
if (hotel == null) {
return R.failed("酒店不存在,请传 hotelId 或 hotelCode");
}
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
return R.failed("酒店已下架,无法下单");
}
String productCode = str(body.get("productCode"));
if (StrUtil.isBlank(productCode)) {
return R.failed("productCode 不能为空");
}
SmHotelProductEntity product = productMapper.selectOne(Wrappers.<SmHotelProductEntity>lambdaQuery()
.eq(SmHotelProductEntity::getHotelId, hotel.getId())
.eq(SmHotelProductEntity::getProductCode, productCode)
.last("limit 1"));
if (product == null) {
return R.failed("产品不存在,请先拉取酒店详情");
}
boolean localHotel = SmHotelDataSource.isLocal(hotel.getSheetName());
if (StrUtil.isBlank(product.getRoomTypeCode())) {
return R.failed("产品缺少房型码");
}
if (!localHotel && StrUtil.isBlank(product.getPriceCode())) {
return R.failed("产品缺少房价码,请重新拉取上游详情");
}
LocalDate checkIn = parseDate(body.get("checkInDate"));
LocalDate checkOut = parseDate(body.get("checkOutDate"));
if (checkIn == null || checkOut == null || !checkIn.isBefore(checkOut)) {
return R.failed("入住/离店日期无效");
}
String contactName = str(body.get("contactName"));
String contactPhone = str(body.get("contactPhone"));
if (StrUtil.hasBlank(contactName, contactPhone)) {
return R.failed("联系人姓名和手机号不能为空");
}
String channelNo = str(body.get("channelNo"));
if (StrUtil.isBlank(channelNo)) {
channelNo = "APP" + System.currentTimeMillis() + (int) (Math.random() * 900 + 100);
}
SmHotelOrderEntity exist = baseMapper.selectOne(Wrappers.<SmHotelOrderEntity>lambdaQuery()
.eq(SmHotelOrderEntity::getChannelNo, channelNo)
.last("limit 1"));
if (exist != null) {
JSONObject existOut = buildOut(exist);
existOut.set("hotelName", hotel.getHotelName());
existOut.set("productName", product.getProductName());
existOut.set("sellAmount", exist.getSellAmount());
existOut.set("amountFen", toAmountFen(exist.getSellAmount()));
return R.ok(existOut, "订单已存在(幂等)");
}
int count = body.get("count") == null ? 1 : Integer.parseInt(String.valueOf(body.get("count")));
if (count < 1) {
count = 1;
}
SmHotelOrderEntity order = new SmHotelOrderEntity();
order.setDownstreamAppId(null);
order.setHotelId(hotel.getId());
order.setChannelNo(channelNo);
order.setGroupCode(hotel.getGroupCode());
order.setHotelCode(hotel.getHotelCode());
order.setRoomTypeCode(StrUtil.blankToDefault(str(body.get("roomTypeCode")), product.getRoomTypeCode()));
order.setProductCode(productCode);
order.setPriceCode(StrUtil.blankToDefault(str(body.get("priceCode")), product.getPriceCode()));
order.setCheckInDate(checkIn);
order.setCheckOutDate(checkOut);
order.setCount(count);
order.setContactName(contactName);
order.setContactPhone(contactPhone);
if (body.get("guestList") != null) {
order.setGuestJson(JSONUtil.toJsonStr(body.get("guestList")));
}
else {
Map<String, Object> guest = new HashMap<>(2);
guest.put("name", contactName);
guest.put("mobile", contactPhone);
order.setGuestJson(JSONUtil.toJsonStr(List.of(guest)));
}
order.setMarkupPercent(localHotel ? BigDecimal.ZERO : priceConfigService.currentMarkupPercent());
if (product.getCostPrice() != null) {
order.setCostAmount(product.getCostPrice().multiply(BigDecimal.valueOf(count)));
}
order.setOrderStatus("CREATED");
order.setPayStatus(PAY_UNPAID);
order.setCompensateStatus(COMP_NONE);
order.setInventoryStatus(SmHotelInventoryStatus.NONE);
order.setCompensateRound(0);
order.setReqJson(JSONUtil.toJsonStr(body));
if (localHotel) {
String precheck = precheckLocalInventory(product.getId(), checkIn, checkOut, count);
if (precheck != null) {
return R.failed(precheck);
}
// 待支付阶段按日历卖价锁定应付金额,支付成功履约时再占用库存
BigDecimal sellSum = sumLocalSellAmount(product.getId(), checkIn, checkOut, count);
if (sellSum != null) {
order.setSellAmount(sellSum);
}
}
else {
// 上游:优先用前端合计(含间夜),否则按产品单价×间数×间夜
if (body.get("totalAmount") != null) {
order.setSellAmount(toDecimal(body.get("totalAmount")));
}
else if (product.getSellPrice() != null) {
long nights = java.time.temporal.ChronoUnit.DAYS.between(checkIn, checkOut);
if (nights < 1) {
nights = 1;
}
order.setSellAmount(product.getSellPrice()
.multiply(BigDecimal.valueOf(count))
.multiply(BigDecimal.valueOf(nights)));
}
}
if (order.getSellAmount() == null || order.getSellAmount().compareTo(BigDecimal.ZERO) <= 0) {
return R.failed("应付金额无效,无法发起支付");
}
baseMapper.insert(order);
JSONObject out = buildOut(order);
out.set("hotelName", hotel.getHotelName());
out.set("productName", product.getProductName());
out.set("sellAmount", order.getSellAmount());
out.set("amountFen", toAmountFen(order.getSellAmount()));
out.set("hotelDeptId", hotel.getDeptId());
return R.ok(out, "待支付订单已创建");
}
@Override
public SmHotelOrderPayInfoVo getPayInfo(Long orderId) {
SmHotelOrderPayInfoVo vo = new SmHotelOrderPayInfoVo();
if (orderId == null) {
vo.setPayable(Boolean.FALSE);
return vo;
}
SmHotelOrderEntity order = baseMapper.selectById(orderId);
if (order == null) {
vo.setPayable(Boolean.FALSE);
return vo;
}
vo.setOrderId(order.getId());
vo.setUserId(parseAppUserId(order.getChannelNo()));
vo.setAmountFen(toAmountFen(order.getSellAmount()));
vo.setPayStatus(order.getPayStatus());
vo.setOrderStatus(order.getOrderStatus());
// 收款商户跟酒店部门,不跟下单用户部门
if (order.getHotelId() != null) {
SmHotelEntity hotel = hotelMapper.selectById(order.getHotelId());
if (hotel != null) {
vo.setHotelDeptId(hotel.getDeptId());
}
}
boolean payable = PAY_UNPAID.equals(order.getPayStatus())
&& "CREATED".equals(order.getOrderStatus())
&& vo.getUserId() != null
&& vo.getHotelDeptId() != null
&& vo.getAmountFen() != null
&& vo.getAmountFen() > 0;
vo.setPayable(payable);
return vo;
}
/**
* 支付成功履约:本地酒店扣日历库存确认;上游酒店再调上游下单。幂等可重入。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean onAppPaySuccess(Long orderId) {
if (orderId == null) {
return false;
}
SmHotelOrderEntity order = baseMapper.selectById(orderId);
if (order == null) {
log.error("|酒店支付成功|订单不存在|orderId={}|", orderId);
return false;
}
// 已履约成功:幂等返回
if (PAY_PAID.equals(order.getPayStatus())
&& ("UPSTREAM_OK".equals(order.getOrderStatus())
|| "PENDING".equals(order.getOrderStatus())
|| "PENDING_CONFIRM".equals(order.getOrderStatus())
|| "CHECKED_IN".equals(order.getOrderStatus()))) {
return true;
}
if (!PAY_UNPAID.equals(order.getPayStatus()) || !"CREATED".equals(order.getOrderStatus())) {
log.warn("|酒店支付成功|状态不可履约|orderId={}|payStatus={}|orderStatus={}|", orderId,
order.getPayStatus(), order.getOrderStatus());
return false;
}
SmHotelEntity hotel = hotelMapper.selectById(order.getHotelId());
if (hotel == null) {
log.error("|酒店支付成功|酒店不存在|orderId={}|hotelId={}|", orderId, order.getHotelId());
return false;
}
SmHotelProductEntity product = productMapper.selectOne(Wrappers.<SmHotelProductEntity>lambdaQuery()
.eq(SmHotelProductEntity::getHotelId, order.getHotelId())
.eq(SmHotelProductEntity::getProductCode, order.getProductCode())
.last("limit 1"));
boolean localHotel = SmHotelDataSource.isLocal(hotel.getSheetName());
if (localHotel) {
if (product == null) {
log.error("|酒店支付成功|本地履约缺产品|orderId={}|", orderId);
return false;
}
// 本地数据源:只确认本地库存,不回调上游
R<JSONObject> local = finishLocalOrder(order, product);
order = baseMapper.selectById(orderId);
if (order == null) {
return false;
}
order.setPayStatus(PAY_PAID);
baseMapper.updateById(order);
log.info("|酒店支付成功|本地履约完成|orderId={}|ok={}|", orderId, local != null && local.isOk());
return local != null && local.isOk();
}
if (product == null) {
order.setOrderStatus("UPSTREAM_FAIL");
order.setPayStatus(PAY_PAID);
order.setFailMsg("支付成功但产品不存在,无法提交上游");
baseMapper.updateById(order);
return false;
}
Map<String, Object> reqBody = StrUtil.isBlank(order.getReqJson()) ? Map.of()
: JSONUtil.parseObj(order.getReqJson());
com.alibaba.fastjson.JSONObject upstreamBody = buildUpstreamOrderBody(order, product, reqBody);
try {
String availMsg = doubleCheckAvailability(upstreamBody, order.getCount());
if (availMsg != null) {
order.setOrderStatus("UPSTREAM_FAIL");
order.setPayStatus(PAY_PAID);
order.setFailMsg(StrUtil.sub(availMsg, 0, 500));
baseMapper.updateById(order);
return false;
}
}
catch (Exception e) {
log.warn("|酒店支付成功|可定校验异常,继续下单|orderId={}|msg={}|", orderId, e.getMessage());
order.setLastCompensateMsg("可定校验异常已忽略: " + StrUtil.sub(e.getMessage(), 0, 200));
}
ShangyouOrderCallResult call = upstreamClient.createOrderRaw(upstreamBody);
applyCreateResult(order, call, upstreamBody);
order.setPayStatus(PAY_PAID);
baseMapper.updateById(order);
boolean hardFail = "UPSTREAM_FAIL".equals(order.getOrderStatus())
&& !COMP_WAITING.equals(order.getCompensateStatus());
log.info("|酒店支付成功|上游履约|orderId={}|status={}|hardFail={}|", orderId, order.getOrderStatus(),
hardFail);
// 支付已成功:即使上游暂时失败也返回 true,避免支付回调无限重试;补偿任务会继续处理
return true;
}
/** channelNo 形如 APP{userId}{13位毫秒时间戳} */
private Long parseAppUserId(String channelNo) {
if (StrUtil.isBlank(channelNo) || !channelNo.startsWith("APP") || channelNo.length() <= 16) {
return null;
}
String rest = channelNo.substring(3);
if (rest.length() <= 13) {
return null;
}
String userIdStr = rest.substring(0, rest.length() - 13);
try {
return Long.valueOf(userIdStr);
}
catch (Exception e) {
return null;
}
}
private Integer toAmountFen(BigDecimal yuan) {
if (yuan == null || yuan.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}
return yuan.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).intValue();
}
/**
* 本地下单前只读校验日历库存(不占用 sold)。
*
@@ -1021,6 +1339,18 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
baseMapper.updateById(order);
return R.ok(true, "本地订单已取消并回补库存");
}
// App 待支付订单:尚未占用库存、未调上游,直接取消
if (StrUtil.isBlank(order.getPlatformNo()) && PAY_UNPAID.equals(order.getPayStatus())
&& "CREATED".equals(order.getOrderStatus())) {
order.setOrderStatus("CANCELLED");
order.setCompensateStatus(COMP_DONE);
order.setNextCompensateTime(null);
if (StrUtil.isNotBlank(reason)) {
order.setFailMsg(StrUtil.sub(reason, 0, 200));
}
baseMapper.updateById(order);
return R.ok(true, "未支付订单已取消");
}
if (StrUtil.isBlank(order.getPlatformNo())) {
return R.failed("尚无上游订单号");
}
@@ -18,6 +18,8 @@ public enum ThirdPayOrderBizTypeEnum {
APP_SHOP_PAY("30", "APP支付-购买商品(有订单)"),
GUARDIAN_WARD_WALLET_RECHARGE("40", "监护人代被监护人饭卡充值"),
XFJ("50", "食堂或者超市的消费机支付"),
/** App 酒店预订支付:业务订单在 supermarket,本服务只记支付流水 */
HOTEL_ORDER_PAY("60", "酒店订单支付"),
;
private final String type;
+6
View File
@@ -197,6 +197,12 @@
<version>5.7.0</version>
<scope>compile</scope>
</dependency>
<!-- 酒店订单支付成功回调 Feign -->
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-supermarket-api</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-integrated-api</artifactId>
@@ -29,6 +29,8 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateShopStatusDto;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import com.bsd.pjcloud.hard.api.feign.RemoteHardOrderService;
import com.bsd.pjcloud.supermarket.api.feign.RemoteSmHotelOrderService;
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo;
import com.bsd.pjcloud.thirdparty.config.AppPlusPayConfig;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformLoginErrorEnum;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformPayType;
@@ -241,6 +243,11 @@ public class ThirdAppPlusServiceImpl implements ThirdAppPlusService {
*/
private final RemoteHardOrderService remoteHardOrderService;
/**
* 酒店订单远程服务,仅供 App 酒店预订支付发起校验与成功履约。
*/
private final RemoteSmHotelOrderService remoteSmHotelOrderService;
/**
* 支付创建前验证并固定商户平台分佣关系。
*/
@@ -413,6 +420,8 @@ public class ThirdAppPlusServiceImpl implements ThirdAppPlusService {
throw new I18nBizException(ErrorCodes.THIRD_PAY_AMOUNT_GT_ZERO);
}
mchManage = resolveShopPayMerchant(userId, dto);
} else if (ObjectUtil.equals(dto.getBizType(), ThirdPayOrderBizTypeEnum.HOTEL_ORDER_PAY)) {
mchManage = resolveHotelPayMerchant(userId, dto);
} else {
throw new I18nBizException(ErrorCodes.COMMON_UNSUPPORTED_PAY_TYPE);
}
@@ -540,6 +549,39 @@ public class ThirdAppPlusServiceImpl implements ThirdAppPlusService {
* @param dto 小程序支付请求
* @return 已校验的商城收款商户
*/
/**
* 酒店订单支付:校验订单归属与金额后,按【酒店所属部门】解析收款商户(非下单用户部门)。
*/
private MchManageVo resolveHotelPayMerchant(Long userId, ThirdMiniAppPayDto dto) {
Long orderId;
try {
orderId = Long.valueOf(dto.getBizId());
} catch (Exception exception) {
throw new I18nBizException(ErrorCodes.THIRD_SHOP_ORDER_BIZ_ID_INVALID);
}
R<SmHotelOrderPayInfoVo> payInfoResult = remoteSmHotelOrderService.getPayInfo(orderId);
SmHotelOrderPayInfoVo payInfo = payInfoResult == null || !payInfoResult.isOk() ? null : payInfoResult.getData();
if (payInfo == null || !Boolean.TRUE.equals(payInfo.getPayable())
|| !ObjectUtil.equals(payInfo.getUserId(), userId)
|| payInfo.getHotelDeptId() == null
|| payInfo.getAmountFen() == null || payInfo.getAmountFen() <= 0) {
throw new I18nBizException(ErrorCodes.THIRD_SHOP_ORDER_NO_PAY);
}
if (dto.getAmount() != null && ObjectUtil.notEqual(dto.getAmount(), payInfo.getAmountFen())) {
throw new I18nBizException(ErrorCodes.THIRD_SHOP_ORDER_AMOUNT_MISMATCH);
}
// 金额以业务订单为准,禁止客户端伪造
dto.setAmount(payInfo.getAmountFen());
// 仅查酒店类型商户,与食堂/商超隔离
R<MchManageVo> merchantResult = remoteMchService.getViewsMchByDeptId(payInfo.getHotelDeptId(),
List.of(MerchantTypeEnum.HOTEL));
MchManageVo merchant = merchantResult == null || !merchantResult.isOk() ? null : merchantResult.getData();
if (ObjectUtil.isEmpty(merchant)) {
throw new I18nBizException(ErrorCodes.THIRD_USER_NO_MCH);
}
return merchant;
}
private MchManageVo resolveShopPayMerchant(Long userId, ThirdMiniAppPayDto dto) {
if (!(dto instanceof ThirdMiniRemotePayDto remotePayDto) || remotePayDto.getMchId() == null) {
throw new I18nBizException(ErrorCodes.THIRD_SHOP_PAY_MCH_MISSING);
@@ -1665,6 +1707,14 @@ public class ThirdAppPlusServiceImpl implements ThirdAppPlusService {
log.error("|APP支付-购买商品(有订单)|处理小程序支付成功回调异常", e);
return false;
}
} else if (ObjectUtil.equals(thirdPayOrder.getBizType(), ThirdPayOrderBizTypeEnum.HOTEL_ORDER_PAY.getType())) {
try {
R<Boolean> result = remoteSmHotelOrderService.paySuccess(Long.valueOf(thirdPayOrder.getBizId()));
return result != null && result.isOk() && Boolean.TRUE.equals(result.getData());
} catch (Exception e) {
log.error("|酒店订单支付|处理支付成功回调异常", e);
return false;
}
} else {
return true;
}
@@ -131,6 +131,9 @@ class ThirdAppPlusServiceImplOrderCodePayTest {
@Mock
private RemoteHardOrderService remoteHardOrderService;
@Mock
private com.bsd.pjcloud.supermarket.api.feign.RemoteSmHotelOrderService remoteSmHotelOrderService;
@InjectMocks
private ThirdAppPlusServiceImpl service;
@@ -279,23 +279,28 @@ public class MchManageController {
@GetMapping("/select")
public R getSelect(@ParameterObject Page page, @ParameterObject MchManageDto dto) {
DataScope dataScope = null;
PjcloudUser user = SecurityUtils.getUser();
List<MerchantTypeEnum> typeList = new ArrayList<>();
if (ObjectUtil.isNotEmpty(dto.getTypes())) {
MerchantTypeEnum targeTypes = MerchantTypeEnum.valueOfByType(dto.getTypes());
if (ObjectUtil.isNotEmpty(targeTypes)) {
typeList.add(targeTypes);
// 按主键精确回显时不要叠加「当前用户食堂商户」ids,否则酒店等非食堂类型无法回显名称
if (dto.getId() == null) {
PjcloudUser user = SecurityUtils.getUser();
List<MerchantTypeEnum> typeList = new ArrayList<>();
if (ObjectUtil.isNotEmpty(dto.getTypes())) {
MerchantTypeEnum targeTypes = MerchantTypeEnum.valueOfByType(dto.getTypes());
if (ObjectUtil.isNotEmpty(targeTypes)) {
typeList.add(targeTypes);
}
}
if (typeList.isEmpty()) {
typeList = List.of(MerchantTypeEnum.CANTEEN);
}
List<Long> selfMchIdList = mchManageService.getSelfMchIdList(user.getId(), typeList);
if (ObjectUtil.isNotEmpty(selfMchIdList)) {
List<Long> ids = dto.getIds();
if (ObjectUtil.isEmpty(ids)) {
ids = new ArrayList<>();
}
ids.addAll(selfMchIdList);
dto.setIds(ids);
}
}
if (typeList.isEmpty()) {
typeList = List.of(MerchantTypeEnum.CANTEEN);
}
List<Long> selfMchIdList = mchManageService.getSelfMchIdList(user.getId(), typeList);
if (ObjectUtil.isNotEmpty(selfMchIdList)) {
List<Long> ids = dto.getIds();
if (ObjectUtil.isEmpty(ids)) ids = new ArrayList<>();
ids.addAll(selfMchIdList);
dto.setIds(ids);
}
return R.ok(mchManageService.scopePageList(page, dto, dataScope));
}
@@ -1,7 +1,9 @@
package com.bsd.pjcloud.wallet.controller.mch;
import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.constant.enums.MerchantTypeEnum;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.log.annotation.SysLog;
@@ -19,6 +21,7 @@ import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -38,16 +41,40 @@ public class MchPaySettingController {
/* ************************************* start app start ************************************* */
/**
* 用户可以支付的方式(目前微信、支付宝)
* 可用支付方式:
* 不传 deptId → 登录用户部门下食堂/助农商户;
* 传 deptId + mchTypes → 指定部门下指定类型商户(酒店传 mchTypes=6)。
*/
@Operation(summary = "用户可以支付的方式(目前微信、支付宝)", description = "用户可以支付的方式(目前微信、支付宝)")
@Operation(summary = "用户可以支付的方式(目前微信、支付宝)",
description = "不传 deptId 按登录用户;传 deptId/mchTypes 按指定部门与商户类型")
@ApiCryptoAes
@GetMapping("/app/pay-type-list")
public R<List<MchPaySettingVo>> getUserPaySettingList() {
public R<List<MchPaySettingVo>> getUserPaySettingList(
@RequestParam(required = false) Long deptId,
@RequestParam(required = false) List<String> mchTypes) {
if (deptId != null) {
List<MerchantTypeEnum> typeEnums = resolveMchTypes(mchTypes);
return R.ok(mchPaySettingService.getPaySettingListByDeptId(deptId, typeEnums));
}
PjcloudUser user = SecurityUtils.getUser();
return R.ok(mchPaySettingService.getUserPaySettingList(user));
}
/** 将前端传入的 types 编码转为枚举;空则由 Service 默认食堂+助农 */
private List<MerchantTypeEnum> resolveMchTypes(List<String> mchTypes) {
if (ObjectUtil.isEmpty(mchTypes)) {
return List.of();
}
List<MerchantTypeEnum> result = new ArrayList<>();
for (String type : mchTypes) {
MerchantTypeEnum e = MerchantTypeEnum.valueOfByType(type);
if (e != null) {
result.add(e);
}
}
return result;
}
/* ************************************* end app end ************************************* */
/**
@@ -3,6 +3,7 @@ package com.bsd.pjcloud.wallet.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.enums.MerchantTypeEnum;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.wallet.api.dto.mch.MchPaySettingDto;
@@ -86,4 +87,14 @@ public interface MchPaySettingService extends IService<MchPaySettingEntity> {
* @return 已脱敏的可用支付配置
*/
List<MchPaySettingVo> getUserPaySettingList(PjcloudUser user);
/**
* 按指定部门 + 商户类型解析所属商户后返回可用支付方式。
*
* @param deptId 业务侧指定的部门主键
* @param mchTypeList 商户类型;空则默认食堂+助农(兼容旧调用)
* @return 已脱敏的可用支付配置;部门无商户或未配置时返回空列表
*/
List<MchPaySettingVo> getPaySettingListByDeptId(Long deptId, List<MerchantTypeEnum> mchTypeList);
}
@@ -149,7 +149,23 @@ public class MchPaySettingServiceImpl extends ServiceImpl<MchPaySettingMapper, M
if (ObjectUtil.isEmpty(userInfo)) {
throw new RuntimeException("用户不存在");
}
MchManageVo targetMch = mchManageService.getViewsCanteenMch(userInfo.getSysUser().getDeptId(), List.of(MerchantTypeEnum.CANTEEN, MerchantTypeEnum.FARMER));
// 默认仍按食堂/助农,与酒店类型隔离
return getPaySettingListByDeptId(userInfo.getSysUser().getDeptId(),
List.of(MerchantTypeEnum.CANTEEN, MerchantTypeEnum.FARMER));
}
/**
* 按部门 + 商户类型解析支付配置;酒店传 HOTEL,避免与食堂/商超混用。
*/
@Override
public List<MchPaySettingVo> getPaySettingListByDeptId(Long deptId, List<MerchantTypeEnum> mchTypeList) {
if (deptId == null) {
return List.of();
}
List<MerchantTypeEnum> types = ObjectUtil.isEmpty(mchTypeList)
? List.of(MerchantTypeEnum.CANTEEN, MerchantTypeEnum.FARMER)
: mchTypeList;
MchManageVo targetMch = mchManageService.getViewsCanteenMch(deptId, types);
if (ObjectUtil.isEmpty(targetMch) || ObjectUtil.isEmpty(targetMch.getPaySettingList())) {
return List.of();
}