1.酒店 2.商户增加酒店类型,部门接口适配酒店
This commit is contained in:
1 parent
074340e5c6
commit
8ef2f8ccd6
15 files changed
+593
-47
No files matched your search
+2
@@ -16,6 +16,8 @@ public enum MerchantTypeEnum {
|
||||
SELF_SUPERMARKET("3", "自营商家"),
|
||||
THIRD_APP("4", "第三方应用"),
|
||||
FARMER("5", "共富商城(助农)"),
|
||||
/** 酒店预订收款商户,与食堂/商超隔离 */
|
||||
HOTEL("6", "酒店"),
|
||||
;
|
||||
|
||||
/**
|
||||
|
||||
+31
@@ -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);
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
+3
-27
@@ -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 = "我的酒店订单分页")
|
||||
|
||||
+40
@@ -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));
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
|
||||
|
||||
+330
@@ -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("尚无上游订单号");
|
||||
}
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
+50
@@ -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;
|
||||
}
|
||||
|
||||
+3
@@ -131,6 +131,9 @@ class ThirdAppPlusServiceImplOrderCodePayTest {
|
||||
@Mock
|
||||
private RemoteHardOrderService remoteHardOrderService;
|
||||
|
||||
@Mock
|
||||
private com.bsd.pjcloud.supermarket.api.feign.RemoteSmHotelOrderService remoteSmHotelOrderService;
|
||||
|
||||
@InjectMocks
|
||||
private ThirdAppPlusServiceImpl service;
|
||||
|
||||
|
||||
+21
-16
@@ -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));
|
||||
}
|
||||
|
||||
+30
-3
@@ -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 ************************************* */
|
||||
|
||||
/**
|
||||
|
||||
+11
@@ -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);
|
||||
}
|
||||
|
||||
+17
-1
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user