diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java
index fe00ceef7..f900a2c01 100644
--- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java
+++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/DailyMenuAppVO.java
@@ -93,7 +93,7 @@ public class DailyMenuAppVO {
/**
* 当前可售库存。
- *
App/移动端:移动端可预订剩余;AD:总库存 − 移动端已预订。字段名保持 stock 兼容旧客户端。
+ *
App/移动端:移动端可预订剩余;AD:剩余总库存(总库存 − 线上已订 − 线下已售)。字段名保持 stock 兼容旧客户端。
*/
private Integer stock;
@@ -112,7 +112,7 @@ public class DailyMenuAppVO {
/** 移动端可预订剩余 */
private Integer remainOnlineStock;
- /** App:剩余总库存;AD:与 stock 同为 总库存−移动端已预订 */
+ /** 剩余总库存(AD 与 stock 同值;管理端 remainStock 同口径) */
private Integer remainOfflineStock;
/**
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java
index d8b411975..8049b9af8 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java
@@ -385,7 +385,7 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl getCanteenMenuListScope(String day, Integer hourType, Long categoryId, Long merchantId) {
List list = baseMapper.getCanteenMenuList(day, hourType, categoryId, merchantId, DataScope.of());
fillDailyMenuAppCombo(list);
- // 安卓消费机:stock = 当日线下剩余
+ // 安卓消费机:stock = 剩余总库存(与管理端 remainStock、线下扣减边界同口径)
LocalDate mealDate = null;
try {
mealDate = day == null ? null : LocalDate.parse(day);
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuStockSupport.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuStockSupport.java
index 63e7eb8d9..5588b7e89 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuStockSupport.java
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuStockSupport.java
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
*
* 剩余总库存 = max(0, 总库存 − 移动端已预订 − 消费机已销售)
* 移动端可预订剩余 = min(移动端上限 − 已预订, 剩余总库存)
- * AD stock = max(0, 总库存 − 移动端已预订)
+ * AD stock = 剩余总库存(与管理端 remainStock、线下扣减边界同口径)
*
* PC 管理端只展示总库存、移动端上限、移动端已订、消费机已售、剩余总库存。
* 扣减仍受「剩余总库存」边界约束,防止超卖。
@@ -95,7 +95,8 @@ public class CanteenDailyMenuStockSupport {
}
/**
- * AD 返回的 stock:总库存 − 移动端已预订(不含消费机已售;已售由 offline_sold 单独体现)。
+ * 总库存 − 移动端已预订(不含消费机已售)。
+ * 仅作库存扣减流程的线下游标基线;AD 列表展示已改用 {@link #sharedRemain}。
*/
public static int adStockOf(Integer totalStock, Integer onlineSold) {
return Math.max(0, totalStockOf(totalStock) - soldOrDefault(onlineSold));
@@ -126,7 +127,7 @@ public class CanteenDailyMenuStockSupport {
applyAppComboLinkedRemain(rows, true);
}
- /** 安卓消费机:stock = 总库存 − 移动端已预订 */
+ /** 安卓消费机:stock = 剩余总库存(管理端 remainStock 同口径) */
public void fillAdOfflineRemain(List rows, Long merchantId, LocalDate mealDate) {
if (CollUtil.isEmpty(rows)) {
return;
@@ -206,15 +207,14 @@ public class CanteenDailyMenuStockSupport {
int offlineSold = soldOrDefault(row.getOfflineSold());
int shared = sharedRemain(total, onlineSold, offlineSold);
int onlineRemain = onlineRemain(total, mobile, onlineSold, offlineSold);
- int adStock = adStockOf(total, onlineSold);
row.setOnlineStock(mobile);
row.setOfflineStock(total);
row.setOnlineSold(onlineSold);
row.setOfflineSold(offlineSold);
row.setRemainOnlineStock(onlineRemain);
- // AD:remainOfflineStock / stock = 总库存 − 线上已订;App 侧 remainOffline 仍给剩余总库存供参考
- row.setRemainOfflineStock(stockAsOnline ? shared : adStock);
- row.setStock(stockAsOnline ? onlineRemain : adStock);
+ // remainOfflineStock = 剩余总库存;stock:App=移动端可订剩余,AD=剩余总库存(与线下扣减边界一致)
+ row.setRemainOfflineStock(shared);
+ row.setStock(stockAsOnline ? onlineRemain : shared);
}
/**
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/dto/appmessage/AppPushDeliveryRetryDto.java b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/dto/appmessage/AppPushDeliveryRetryDto.java
new file mode 100644
index 000000000..755fa78ea
--- /dev/null
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/dto/appmessage/AppPushDeliveryRetryDto.java
@@ -0,0 +1,25 @@
+package com.bsd.pjcloud.thirdparty.dto.appmessage;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * PC重新推送失败终态投递记录的请求。
+ */
+@Data
+public class AppPushDeliveryRetryDto implements Serializable {
+
+ /** 当前操作的商户ID,用于复核管理员数据范围。 */
+ @NotNull(message = "商户ID不能为空")
+ private Long mchId;
+
+ /** 待重新推送的投递记录ID列表,仅失败终态记录会被重置。 */
+ @NotEmpty(message = "请选择要重新推送的投递记录")
+ @Size(max = 100, message = "单次最多重新推送100条投递记录")
+ private List deliveryIds;
+}
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/vo/appmessage/AppPushDeliveryVo.java b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/vo/appmessage/AppPushDeliveryVo.java
index f8c34ca5a..e9c7e9780 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/vo/appmessage/AppPushDeliveryVo.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/vo/appmessage/AppPushDeliveryVo.java
@@ -23,6 +23,9 @@ public class AppPushDeliveryVo implements Serializable {
/** 目标用户ID。 */
private Long recipientUserId;
+ /** 接收用户姓名,远程用户服务不可用或用户已删除时为空。 */
+ private String recipientName;
+
/** 投递状态。 */
private String status;
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appmessage/ThirdAppMessagePcController.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appmessage/ThirdAppMessagePcController.java
index dd24ba9f7..f9b589a03 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appmessage/ThirdAppMessagePcController.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appmessage/ThirdAppMessagePcController.java
@@ -6,6 +6,7 @@ import com.bsd.pjcloud.common.core.constant.enums.MerchantTypeEnum;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
+import com.bsd.pjcloud.thirdparty.dto.appmessage.AppPushDeliveryRetryDto;
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppPushPolicySaveDto;
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppPushTestDto;
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdAppMessageSettingService;
@@ -121,6 +122,7 @@ public class ThirdAppMessagePcController {
* @param mchId 商户ID
* @param status 可选投递状态
* @param eventCode 可选事件编码
+ * @param recipientUserId 可选接收用户ID
* @return 商户投递记录分页
*/
@Operation(summary = "分页查询商户业务Push投递记录")
@@ -128,9 +130,10 @@ public class ThirdAppMessagePcController {
@GetMapping("/deliveries")
public R> pageDeliveries(@ParameterObject Page> page, @RequestParam Long mchId,
@RequestParam(required = false) String status,
- @RequestParam(required = false) String eventCode) {
+ @RequestParam(required = false) String eventCode,
+ @RequestParam(required = false) Long recipientUserId) {
requireMerchantAccess(mchId);
- return R.ok(deliveryService.page(page, mchId, status, eventCode));
+ return R.ok(deliveryService.page(page, mchId, status, eventCode, recipientUserId));
}
/**
@@ -148,6 +151,21 @@ public class ThirdAppMessagePcController {
return R.ok(deliveryService.listAttempts(deliveryId, mchId));
}
+ /**
+ * 把失败终态的投递记录重置回待投递,由投递扫描任务在数秒内重新推送。
+ *
+ * @param dto 商户与投递记录ID集合
+ * @return 实际重置的记录数量
+ */
+ @Operation(summary = "重新推送App投递记录")
+ @SysLog("重新推送App投递记录")
+ @PreAuthorize("@pms.hasPermission('wallet_mchManage_setting')")
+ @PostMapping("/deliveries/retry")
+ public R retryDeliveries(@Valid @RequestBody AppPushDeliveryRetryDto dto) {
+ requireMerchantAccess(dto.getMchId());
+ return R.ok(deliveryService.retryDeliveries(dto.getMchId(), dto.getDeliveryIds()));
+ }
+
/**
* 查询当前部署Push供应商只读状态。
*
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/appmessage/ThirdPushDeliveryService.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/appmessage/ThirdPushDeliveryService.java
index 0bc601a08..85690c424 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/appmessage/ThirdPushDeliveryService.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/appmessage/ThirdPushDeliveryService.java
@@ -35,9 +35,10 @@ public interface ThirdPushDeliveryService {
* @param mchId 商户ID
* @param status 可选投递状态
* @param eventCode 可选事件编码
+ * @param recipientUserId 可选接收用户ID
* @return 商户投递记录分页
*/
- Page page(Page> page, Long mchId, String status, String eventCode);
+ Page page(Page> page, Long mchId, String status, String eventCode, Long recipientUserId);
/**
* 查询指定商户投递任务的供应商调用详情。
@@ -48,6 +49,15 @@ public interface ThirdPushDeliveryService {
*/
List listAttempts(Long deliveryId, Long mchId);
+ /**
+ * 把失败终态的投递记录重置回待投递,由既有扫描任务在数秒内重新推送。
+ *
+ * @param mchId 商户ID
+ * @param deliveryIds 待重新推送的投递记录ID
+ * @return 实际重置的记录数量,非失败终态或非本商户记录不计入
+ */
+ int retryDeliveries(Long mchId, List deliveryIds);
+
/**
* 返回供应商只读健康状态,不包含部署密钥。
*
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appmessage/ThirdPushDeliveryServiceImpl.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appmessage/ThirdPushDeliveryServiceImpl.java
index 2cd47554a..7cb353777 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appmessage/ThirdPushDeliveryServiceImpl.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appmessage/ThirdPushDeliveryServiceImpl.java
@@ -1,9 +1,14 @@
package com.bsd.pjcloud.thirdparty.service.impl.appmessage;
+import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.bsd.pjcloud.admin.api.entity.SysUser;
+import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
+import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
import com.bsd.pjcloud.thirdparty.constant.appmessage.AppMessageConstants;
import com.bsd.pjcloud.thirdparty.constant.push.ThirdPushBizTypeEnum;
@@ -83,6 +88,9 @@ public class ThirdPushDeliveryServiceImpl implements ThirdPushDeliveryService {
/** 事务模板,保证任务领取、供应商尝试留痕与最终投递状态在同一数据库事务中提交。 */
private final TransactionTemplate transactionTemplate;
+ /** UPMS远程用户服务,用于PC列表补齐接收用户姓名。 */
+ private final RemoteUserService remoteUserService;
+
/**
* 创建一条唯一Push决策;没有有效设备时把原本待投递任务记录为跳过。
*
@@ -157,23 +165,56 @@ public class ThirdPushDeliveryServiceImpl implements ThirdPushDeliveryService {
* @param mchId 商户ID
* @param status 可选投递状态
* @param eventCode 可选事件编码
+ * @param recipientUserId 可选接收用户ID
* @return PC投递记录分页
*/
@Override
- public Page page(Page> page, Long mchId, String status, String eventCode) {
+ public Page page(Page> page, Long mchId, String status, String eventCode, Long recipientUserId) {
Page entityPage = deliveryMapper.selectPage(
new Page<>(page.getCurrent(), page.getSize()),
Wrappers.lambdaQuery()
.eq(ThirdPushDeliveryEntity::getSourceMchId, mchId)
.eq(StrUtil.isNotBlank(status), ThirdPushDeliveryEntity::getStatus, status)
.eq(StrUtil.isNotBlank(eventCode), ThirdPushDeliveryEntity::getEventCode, eventCode)
+ .eq(recipientUserId != null, ThirdPushDeliveryEntity::getRecipientUserId, recipientUserId)
.orderByDesc(ThirdPushDeliveryEntity::getCreateTime)
.orderByDesc(ThirdPushDeliveryEntity::getId));
Page result = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
result.setRecords(entityPage.getRecords().stream().map(this::toDeliveryVo).toList());
+ fillRecipientNames(result.getRecords());
return result;
}
+ /**
+ * 批量补齐投递记录接收用户姓名,远程服务异常或用户已删除不阻断诊断列表。
+ *
+ * @param records 当前页PC投递记录
+ */
+ private void fillRecipientNames(List records) {
+ List userIdList = records.stream().map(AppPushDeliveryVo::getRecipientUserId)
+ .filter(ObjectUtil::isNotEmpty).distinct().toList();
+ if (ObjectUtil.isEmpty(userIdList)) {
+ return;
+ }
+ try {
+ R> result = remoteUserService.getInnerUserById(userIdList);
+ List userList = result == null || !result.isOk() ? null : result.getData();
+ if (ObjectUtil.isEmpty(userList)) {
+ return;
+ }
+ Map userMap = IterUtil.toMap(userList, SysUser::getUserId);
+ for (AppPushDeliveryVo record : records) {
+ SysUser targetUser = userMap.get(record.getRecipientUserId());
+ if (targetUser == null) {
+ continue;
+ }
+ record.setRecipientName(targetUser.getName());
+ }
+ } catch (Exception exception) {
+ log.warn("投递记录列表补充接收用户姓名失败,errorType:{}", exception.getClass().getSimpleName());
+ }
+ }
+
/**
* 查询指定商户投递任务的供应商调用详情,先校验任务确实属于该商户。
*
@@ -196,6 +237,31 @@ public class ThirdPushDeliveryServiceImpl implements ThirdPushDeliveryService {
.stream().map(this::toAttemptVo).toList();
}
+ /**
+ * 把失败终态的投递记录条件重置回待投递,交由既有扫描任务重新领取处理。
+ *
+ * @param mchId 商户ID
+ * @param deliveryIds 待重新推送的投递记录ID
+ * @return 实际重置的记录数量
+ */
+ @Override
+ public int retryDeliveries(Long mchId, List deliveryIds) {
+ if (ObjectUtil.isEmpty(deliveryIds)) {
+ return 0;
+ }
+ return deliveryMapper.update(null, Wrappers.lambdaUpdate()
+ .set(ThirdPushDeliveryEntity::getStatus, AppMessageConstants.DELIVERY_PENDING)
+ .set(ThirdPushDeliveryEntity::getAttemptCount, 0)
+ .set(ThirdPushDeliveryEntity::getNextRetryTime, null)
+ .set(ThirdPushDeliveryEntity::getFinalProviderCode, null)
+ .set(ThirdPushDeliveryEntity::getLastError, null)
+ .set(ThirdPushDeliveryEntity::getSuccessTime, null)
+ .set(ThirdPushDeliveryEntity::getUpdateTime, LocalDateTime.now())
+ .in(ThirdPushDeliveryEntity::getId, deliveryIds)
+ .eq(ThirdPushDeliveryEntity::getSourceMchId, mchId)
+ .eq(ThirdPushDeliveryEntity::getStatus, AppMessageConstants.DELIVERY_FAILED));
+ }
+
/**
* 返回全部供应商只读状态。
*