每日库存

This commit is contained in:
zjf committed 2026-09-11 15:45:02 +08:00
1 parent 2087dd7933
commit c6d7e4cad0
15 files changed
+351 -91

No files matched your search

@@ -46,18 +46,24 @@ public interface RemoteCanteenSpuService {
/**
* 获取菜品列表
* 获取菜品列表(有每日菜谱时 stock=当日线下剩余)
* @param categoryId 不填默认查全部
* @param day 菜谱日期 yyyy-MM-dd,不填默认当天
* @param hourType 餐段,不填按分类解析
* @return
*/
@GetMapping("/canteenSpu/ad/getCanteenSpuList")
R getCanteenSpuList(@RequestParam(value = "categoryId",required = false)String categoryId);
R getCanteenSpuList(@RequestParam(value = "categoryId",required = false)String categoryId,
@RequestParam(value = "day", required = false) String day,
@RequestParam(value = "hourType", required = false) Integer hourType);
/**
* 获取菜品详情(含详情富文本、套餐组成)
* 获取菜品详情(含详情富文本、套餐组成);有每日菜谱时 stock=线下剩余
*/
@GetMapping("/canteenSpu/ad/getCanteenSpuDetails")
R getCanteenSpuDetails(@RequestParam("id") Long id);
R getCanteenSpuDetails(@RequestParam("id") Long id,
@RequestParam(value = "day", required = false) String day,
@RequestParam(value = "hourType", required = false) Integer hourType);
/**
@@ -50,17 +50,29 @@ public class CanteenDailyMenuEntity extends Model<CanteenDailyMenuEntity> {
private Integer hourType;
/**
* 线上可预约库存配额(人工设置;剩余=配额−线上净销量)
* 线上可预约库存配额(人工设置;下单不改;剩余=配额−onlineSold)
*/
@Schema(description = "线上可预约库存配额")
private Integer onlineStock;
/**
* 线下销售库存配额(人工设置;剩余对外字段名仍用 stock)
* 线下销售库存配额(人工设置;下单不改;剩余=配额−offlineSold)
*/
@Schema(description = "线下销售库存配额")
private Integer offlineStock;
/**
* 线上净销量(下单增加、售后完成减少)
*/
@Schema(description = "线上净销量")
private Integer onlineSold;
/**
* 线下净销量(下单增加、售后完成减少)
*/
@Schema(description = "线下净销量")
private Integer offlineSold;
/**
* 商户id
*/
@@ -65,9 +65,9 @@ public class ADCanteenSpuListVO {
/**
* 库存
* 库存:无每日菜谱时为 canteen_spu.stock;有菜谱时 AD=线下剩余、App 详情=线上剩余
*/
@Schema(description="库存")
@Schema(description="库存(有每日菜谱时为通道剩余)")
private Integer stock;
@@ -53,6 +53,12 @@ public class DailyMenuAdminVO {
*/
private Integer stock;
/** 线上净销量(仅服务端填剩余用,可不展示) */
private Integer onlineSold;
/** 线下净销量(仅服务端填剩余用,可不展示) */
private Integer offlineSold;
/** 线上剩余 = onlineStock − 预约净销量 */
private Integer remainOnlineStock;
@@ -82,12 +82,24 @@ public class DailyMenuAppVO {
*/
private Integer stock;
/** 线上配额(可选透出) */
/** 线上配额(人工;下单不改) */
private Integer onlineStock;
/** 线下配额(可选透出) */
/** 线下配额(人工;下单不改) */
private Integer offlineStock;
/** 线上净销量(服务端计算剩余用) */
private Integer onlineSold;
/** 线下净销量(服务端计算剩余用) */
private Integer offlineSold;
/** 线上剩余 = onlineStock − onlineSold */
private Integer remainOnlineStock;
/** 线下剩余 = offlineStock − offlineSold */
private Integer remainOfflineStock;
/**
* 商品销量,仅用于详情页展示。
*/
@@ -26,7 +26,6 @@ import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
import com.bsd.pjcloud.common.core.util.MsgUtils;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
@@ -45,7 +44,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -339,57 +337,68 @@ public class CanteenSpuController {
/**
* 获取菜品列表
* 获取菜品列表(有每日菜谱时 stock=当日线下剩余)
*
* @param categoryId 分类编号,不传默认查询全部
* @return
* @param day 菜谱日期 yyyy-MM-dd,不传默认当天
* @param hourType 餐段,不传则按分类 categoryType 解析
*/
@GetMapping("/ad/getCanteenSpuList")
public R getCanteenSpuList(@RequestParam(value = "categoryId", required = false) String categoryId) {
return canteenSpuService.getCanteenSpuList(categoryId);
public R getCanteenSpuList(@RequestParam(value = "categoryId", required = false) String categoryId,
@RequestParam(value = "day", required = false) String day,
@RequestParam(value = "hourType", required = false) Integer hourType) {
return canteenSpuService.getCanteenSpuList(categoryId, day, hourType);
}
/**
* 查询收银终端使用的每日菜品详情,并补充富文本与套餐组成。
* stock:有每日菜谱行时为线下剩余。
*
* @param id 每日菜品 SPU ID
* @param day 菜谱日期,空则当天
* @param hourType 餐段,空则按分类解析
* @return 每日菜品详情;商品不存在时返回失败响应
*/
@GetMapping("/ad/getCanteenSpuDetails")
public R<ADCanteenSpuListVO> getCanteenSpuDetails(@RequestParam("id") Long id) {
CanteenSpuEntity detail = canteenSpuService.getDetail(id);
if (detail == null) {
public R<ADCanteenSpuListVO> getCanteenSpuDetails(@RequestParam("id") Long id,
@RequestParam(value = "day", required = false) String day,
@RequestParam(value = "hourType", required = false) Integer hourType) {
ADCanteenSpuListVO vo = canteenSpuService.buildAdSpuDetails(id, day, hourType, true);
if (vo == null) {
return R.failed(MsgUtils.getMessage(ErrorCodes.COMMON_PRODUCT_NOT_FOUND));
}
ADCanteenSpuListVO vo = BeanUtils.toBean(detail, ADCanteenSpuListVO.class);
vo.setIntroduction(detail.getIntroduction());
vo.setDescription(detail.getDescription());
int type = detail.getSpuType() == null ? 1 : detail.getSpuType();
vo.setSpuType(type);
if (type == 2) {
vo.setComboItems(canteenSpuService.loadComboItemVos(Collections.singletonList(id))
.getOrDefault(id, Collections.emptyList()));
}
else {
vo.setComboItems(Collections.emptyList());
}
return R.ok(vo);
}
/**
* 按每日菜品 SPU ID 查询 App 商品详情。
* stock:有每日菜谱行时为线上剩余。
*
* @param id 套餐组成返回的 childSpuId,关联 canteen_spu 主键
* @param day 菜谱日期,空则当天
* @param hourType 餐段,空则按分类解析
* @return App 可展示的每日菜品详情;商品不存在时返回失败响应
*/
@ApiCryptoAes
@Operation(summary = "查询每日菜品详情", description = "通过每日菜品 SPU ID 查询富文本和套餐组成")
@GetMapping("/app/getCanteenSpuDetails")
public R<ADCanteenSpuListVO> getAppCanteenSpuDetails(@RequestParam("id") Long id) {
return getCanteenSpuDetails(id);
public R<ADCanteenSpuListVO> getAppCanteenSpuDetails(@RequestParam("id") Long id,
@RequestParam(value = "day", required = false) String day,
@RequestParam(value = "hourType", required = false) Integer hourType) {
ADCanteenSpuListVO vo = canteenSpuService.buildAdSpuDetails(id, day, hourType, false);
if (vo == null) {
return R.failed(MsgUtils.getMessage(ErrorCodes.COMMON_PRODUCT_NOT_FOUND));
}
return R.ok(vo);
}
/**
* 这里有个问题,就是因为每日加了库存,为了不影响原有的安卓消费机调用扣减库存的逻辑
* 因此这里传参还是效验了spuid,因为之前的库存在菜品列表里,所以为什么不直接拿菜谱id去扣减,用「商户+日期+餐段+SPU」去定位
* @param updateStockReqDTO
* @return
*/
@PostMapping("/ad/updateSkuStock")
@SysLog("更新菜品库存")
@PreAuthorize("@pms.hasPermission('canteen_canteenSpu_edit')")
@@ -64,14 +64,14 @@ public interface CanteenDailyMenuMapper extends PjcloudBaseMapper<CanteenDailyMe
@Param("categoryId") Long categoryId, DataScope dataScope);
/**
* 条件更新线上库存;expected 非空时要求更新前等于 expected。
* 条件更新线上净销量(delta 为剩余侧增量:下单负数);expected 为操作前剩余。
*/
int updateOnlineStockDelta(@Param("merchantId") Long merchantId, @Param("mealDate") java.time.LocalDate mealDate,
@Param("hourType") Integer hourType, @Param("spuId") Long spuId, @Param("delta") int delta,
@Param("expected") Integer expected);
/**
* 条件更新线下库存;expected 非空时要求更新前等于 expected。
* 条件更新线下净销量(delta 为剩余侧增量:下单负数);expected 为操作前剩余。
*/
int updateOfflineStockDelta(@Param("merchantId") Long merchantId, @Param("mealDate") java.time.LocalDate mealDate,
@Param("hourType") Integer hourType, @Param("spuId") Long spuId, @Param("delta") int delta,
@@ -8,6 +8,7 @@ import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuSimpleVO;
import com.bsd.pjcloud.canteen.vo.erp.CanteenSpuStockVO;
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
import com.bsd.pjcloud.common.core.util.R;
@@ -42,11 +43,23 @@ public interface CanteenSpuService extends IService<CanteenSpuEntity> {
/**
* 根据菜品分类id获取菜品信息
* @param categoryId
* @return
* 安卓消费机菜品列表;有每日菜谱行时 stock 为当日线下剩余。
*
* @param categoryId 分类,空则全部
* @param day 菜谱日期 yyyy-MM-dd,空则当天
* @param hourType 餐段,空则按分类 categoryType 解析
*/
R getCanteenSpuList(String categoryId);
R getCanteenSpuList(String categoryId, String day, Integer hourType);
/**
* 组装安卓/App 菜品详情 VO,并按通道覆盖每日菜谱剩余库存。
*
* @param id SPU ID
* @param day 菜谱日期,空则当天
* @param hourType 餐段,空则按分类解析
* @param offline true=线下剩余(AD),false=线上剩余(App)
*/
ADCanteenSpuListVO buildAdSpuDetails(Long id, String day, Integer hourType, boolean offline);
/**
@@ -1459,7 +1459,7 @@ public class CanteenDailyMenuImportHelper {
* 解析/创建单品。
* <p>有「菜品SPU ID」→ 按 ID 复用并按需更新;无 ID → 一律新建,不再按菜名复用。
* <p>{@code stock} 仅在显式传入时更新 {@code canteen_spu.stock};每日菜谱导入传 {@code null},
* 当日可售走 {@code canteen_daily_menu.online_stock/offline_stock}。
* 当日可售 = 配额(online_stock/offline_stock) − 销量;导入只写配额,不改销量。
*/
private FindOrCreate findOrCreateDish(Long merchantId, Long deptId, String categoryName, Long defaultCategoryId,
Map<String, Long> categoryMap, String name, String ingredients, Integer priceFen, Integer stock,
@@ -1,21 +1,27 @@
package com.bsd.pjcloud.canteen.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
import com.bsd.pjcloud.canteen.vo.ADCanteenSpuListVO;
import com.bsd.pjcloud.canteen.vo.DailyMenuAdminVO;
import com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 每日菜谱分池库存展示。
* <p>
* {@code online_stock}/{@code offline_stock} 为可售剩余(下单扣减、退货加回);
* App 的 {@code stock}=线上剩余,安卓 AD 的 {@code stock}=线下剩余(字段名兼容旧客户端)。
* {@code online_stock}/{@code offline_stock} 为人工配额(下单不改);
* {@code online_sold}/{@code offline_sold} 为净销量;剩余 = 配额 − 销量。
* App 的 {@code stock}=线上剩余,安卓 AD 的 {@code stock}=线下剩余。
*/
@Component
@RequiredArgsConstructor
@@ -27,17 +33,30 @@ public class CanteenDailyMenuStockSupport {
return quota == null || quota < 0 ? DEFAULT_QUOTA : quota;
}
/** App:stock = 线上剩余 */
public static int soldOrDefault(Integer sold) {
return sold == null || sold < 0 ? 0 : sold;
}
/** 剩余 = max(0, 配额 − 销量) */
public static int remainOf(Integer quota, Integer sold) {
return Math.max(0, quotaOrDefault(quota) - soldOrDefault(sold));
}
public static int channelRemain(CanteenDailyMenuEntity menu, boolean offline) {
if (menu == null) {
return 0;
}
return offline ? remainOf(menu.getOfflineStock(), menu.getOfflineSold())
: remainOf(menu.getOnlineStock(), menu.getOnlineSold());
}
/** App:stock = 线上剩余;onlineStock/offlineStock 仍透出配额 */
public void fillAppOnlineRemain(List<DailyMenuAppVO> rows, Long merchantId, LocalDate mealDate) {
if (CollUtil.isEmpty(rows)) {
return;
}
for (DailyMenuAppVO row : rows) {
int online = quotaOrDefault(row.getOnlineStock());
int offline = quotaOrDefault(row.getOfflineStock());
row.setOnlineStock(online);
row.setOfflineStock(offline);
row.setStock(online);
applyAppQuotasAndRemains(row, true);
}
}
@@ -47,11 +66,7 @@ public class CanteenDailyMenuStockSupport {
return;
}
for (DailyMenuAppVO row : rows) {
int online = quotaOrDefault(row.getOnlineStock());
int offline = quotaOrDefault(row.getOfflineStock());
row.setOnlineStock(online);
row.setOfflineStock(offline);
row.setStock(offline);
applyAppQuotasAndRemains(row, false);
}
}
@@ -65,32 +80,121 @@ public class CanteenDailyMenuStockSupport {
for (Map.Entry<LocalDate, List<DailyMenuAppVO>> e : byDate.entrySet()) {
fillAdOfflineRemain(e.getValue(), merchantId, e.getKey());
}
// 无日期行仍规范默认值
for (DailyMenuAppVO row : rows) {
if (row.getStartTime() == null) {
int online = quotaOrDefault(row.getOnlineStock());
int offline = quotaOrDefault(row.getOfflineStock());
row.setOnlineStock(online);
row.setOfflineStock(offline);
row.setStock(offline);
applyAppQuotasAndRemains(row, false);
}
}
}
/** PC:配额即当前剩余;合计供旧列 remainStock */
/** PC:配额与剩余分列展示 */
public void fillAdminRemain(List<DailyMenuAdminVO> rows, Long merchantId, LocalDate mealDate) {
if (CollUtil.isEmpty(rows)) {
return;
}
for (DailyMenuAdminVO row : rows) {
int online = quotaOrDefault(row.getOnlineStock());
int offline = quotaOrDefault(row.getOfflineStock());
row.setOnlineStock(online);
row.setOfflineStock(offline);
row.setRemainOnlineStock(online);
row.setRemainOfflineStock(offline);
row.setStock(offline);
row.setRemainStock(online + offline);
int onlineQuota = quotaOrDefault(row.getOnlineStock());
int offlineQuota = quotaOrDefault(row.getOfflineStock());
int onlineRemain = remainOf(onlineQuota, row.getOnlineSold());
int offlineRemain = remainOf(offlineQuota, row.getOfflineSold());
row.setOnlineStock(onlineQuota);
row.setOfflineStock(offlineQuota);
row.setRemainOnlineStock(onlineRemain);
row.setRemainOfflineStock(offlineRemain);
row.setStock(offlineRemain);
row.setRemainStock(onlineRemain + offlineRemain);
}
}
private static void applyAppQuotasAndRemains(DailyMenuAppVO row, boolean stockAsOnline) {
int onlineQuota = quotaOrDefault(row.getOnlineStock());
int offlineQuota = quotaOrDefault(row.getOfflineStock());
int onlineRemain = remainOf(onlineQuota, row.getOnlineSold());
int offlineRemain = remainOf(offlineQuota, row.getOfflineSold());
row.setOnlineStock(onlineQuota);
row.setOfflineStock(offlineQuota);
row.setRemainOnlineStock(onlineRemain);
row.setRemainOfflineStock(offlineRemain);
row.setStock(stockAsOnline ? onlineRemain : offlineRemain);
}
/**
* 安卓 SPU 查看接口:若当日菜谱存在对应行,则 {@code stock} 覆盖为线下剩余;无菜谱行则保留 canteen_spu.stock。
*
* @param rows 已组装的 AD VO(含 merchantId、categoryType)
* @param menus 当日(或指定日)相关每日菜谱行
* @param hourTypeOverride 请求显式餐段;为空时从 VO.categoryType 解析
* @param offline true=线下剩余(AD),false=线上剩余(App 详情复用)
*/
public void overlaySpuViewStock(List<ADCanteenSpuListVO> rows, List<CanteenDailyMenuEntity> menus,
Integer hourTypeOverride, boolean offline) {
if (CollUtil.isEmpty(rows) || CollUtil.isEmpty(menus)) {
return;
}
Map<String, CanteenDailyMenuEntity> byKey = new HashMap<>();
for (CanteenDailyMenuEntity menu : menus) {
if (menu == null || menu.getMerchantId() == null || menu.getSpuId() == null || menu.getHourType() == null) {
continue;
}
byKey.put(menuKey(menu.getMerchantId(), menu.getHourType(), menu.getSpuId()), menu);
}
for (ADCanteenSpuListVO row : rows) {
if (row == null || row.getMerchantId() == null || row.getId() == null) {
continue;
}
Integer hourType = hourTypeOverride != null ? hourTypeOverride : parseHourType(row.getCategoryType());
if (hourType == null) {
// 分类未标餐段时:同一商户+SPU 当日若只有一行菜谱,仍可覆盖
CanteenDailyMenuEntity only = findUniqueMenu(menus, row.getMerchantId(), row.getId());
if (only != null) {
row.setStock(channelRemain(only, offline));
}
continue;
}
CanteenDailyMenuEntity menu = byKey.get(menuKey(row.getMerchantId(), hourType, row.getId()));
if (menu != null) {
row.setStock(channelRemain(menu, offline));
}
}
}
private static CanteenDailyMenuEntity findUniqueMenu(List<CanteenDailyMenuEntity> menus, Long merchantId,
Long spuId) {
CanteenDailyMenuEntity found = null;
for (CanteenDailyMenuEntity menu : menus) {
if (menu == null || !Objects.equals(menu.getMerchantId(), merchantId)
|| !Objects.equals(menu.getSpuId(), spuId)) {
continue;
}
if (found != null) {
return null;
}
found = menu;
}
return found;
}
private static String menuKey(Long merchantId, Integer hourType, Long spuId) {
return merchantId + "|" + hourType + "|" + spuId;
}
/** categoryType 支持 "2" 或 "2,3";取第一个有效餐段。 */
public static Integer parseHourType(String categoryType) {
if (StrUtil.isBlank(categoryType)) {
return null;
}
for (String part : categoryType.split(",")) {
String token = StrUtil.trim(part);
if (StrUtil.isBlank(token)) {
continue;
}
try {
return Integer.valueOf(token);
}
catch (Exception ignored) {
return null;
}
}
return null;
}
}
@@ -70,6 +70,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
private final CanteenOnlineSpuMapper canteenOnlineSpuMapper;
private final CanteenSpuComboItemMapper comboItemMapper;
private final CanteenMerchantScopeHelper merchantScopeHelper;
private final CanteenDailyMenuStockSupport dailyMenuStockSupport;
@Override
public Object selectFoodList(LambdaQueryWrapper<CanteenSpuEntity> wrapper) {
@@ -206,7 +207,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
return "ONLINE".equals(ch) || "OFFLINE".equals(ch);
}
/** 非幂等分池扣减(安卓仍调 /ad/updateSkuStock,由硬件补 stockChannel)。 */
/** 非幂等分池扣减:改写 daily_menu 销量字段(安卓仍调 /ad/updateSkuStock,由硬件补 stockChannel)。 */
private R updateDailyMenuChannelStock(CanteenSpuUpdateStockReqDTO dto) {
if (dto.getMerchantId() == null) {
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_BIZ_PARAM_BAD));
@@ -221,11 +222,10 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
CanteenDailyMenuEntity menu = findDailyMenuRow(dto.getMerchantId(), item.getMealDate(), item.getHourType(),
item.getId());
if (menu == null) {
return R.failed(MsgUtils.getMessage(ErrorCodes.COMMON_PRODUCT_NOT_FOUND));
// 分池扣减按「商户+日期+餐段+SPU」查每日菜谱;查不到不是 canteen_spu 不存在
return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_DAILY_MENU_PRODUCT_BAD));
}
int current = offline
? CanteenDailyMenuStockSupport.quotaOrDefault(menu.getOfflineStock())
: CanteenDailyMenuStockSupport.quotaOrDefault(menu.getOnlineStock());
int current = CanteenDailyMenuStockSupport.channelRemain(menu, offline);
if (item.getIncrCount() < 0 && Math.abs(item.getIncrCount()) > current) {
CanteenSpuEntity spu = baseMapper.selectById(item.getId());
return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_PRODUCT_STOCK_LOW,
@@ -245,7 +245,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
return R.ok();
}
/** 幂等分池扣减:改写 daily_menu.online_stock / offline_stock。 */
/** 幂等分池扣减:改写 daily_menu.online_sold / offline_sold(配额字段不动)。 */
private R updateDailyMenuChannelStockIdempotent(CanteenSpuUpdateStockReqDTO dto, boolean applyOperation,
boolean rollbackOperation) {
if (dto.getMerchantId() == null
@@ -265,11 +265,9 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
CanteenDailyMenuEntity menu = findDailyMenuRow(dto.getMerchantId(), item.getMealDate(), item.getHourType(),
item.getId());
if (menu == null) {
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_SPU_BAD));
return R.failed(MsgUtils.getMessage(ErrorCodes.HARD_DAILY_MENU_PRODUCT_BAD));
}
int current = offline
? CanteenDailyMenuStockSupport.quotaOrDefault(menu.getOfflineStock())
: CanteenDailyMenuStockSupport.quotaOrDefault(menu.getOnlineStock());
int current = CanteenDailyMenuStockSupport.channelRemain(menu, offline);
int expectedStock = item.getBeforeStock();
if (expectedStock < 0) {
return R.failed(MsgUtils.getMessage(ErrorCodes.CANTEEN_STOCK_SNAPSHOT_BAD));
@@ -367,7 +365,7 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
}
@Override
public R getCanteenSpuList(String categoryId) {
public R getCanteenSpuList(String categoryId, String day, Integer hourType) {
List<CanteenSpuEntity> canteenSpuEntities = baseMapper.selectListByScope(new LambdaQueryWrapper<CanteenSpuEntity>()
.eq(StringUtils.isNotEmpty(categoryId), CanteenSpuEntity::getCategoryId, categoryId), DataScope.of());
if (CollectionUtils.isEmpty(canteenSpuEntities)) {
@@ -404,9 +402,73 @@ public class CanteenSpuServiceImpl extends ServiceImpl<CanteenSpuMapper, Canteen
item.setCategoryType(listMap.get(item.getCategoryId()).get(0).getCategoryType());
}
}
// 有每日菜谱则 stock=线下剩余,避免安卓仍展示 canteen_spu.stock
overlayDailyRemainOnAdVos(bean, day, hourType, true);
return R.ok(bean);
}
@Override
public ADCanteenSpuListVO buildAdSpuDetails(Long id, String day, Integer hourType, boolean offline) {
CanteenSpuEntity detail = getDetail(id);
if (detail == null) {
return null;
}
ADCanteenSpuListVO vo = BeanUtils.toBean(detail, ADCanteenSpuListVO.class);
vo.setIntroduction(detail.getIntroduction());
vo.setDescription(detail.getDescription());
int type = detail.getSpuType() == null ? 1 : detail.getSpuType();
vo.setSpuType(type);
if (type == 2) {
vo.setComboItems(loadComboItemVos(Collections.singletonList(id)).getOrDefault(id, Collections.emptyList()));
}
else {
vo.setComboItems(Collections.emptyList());
}
if (detail.getCategoryId() != null) {
CanteenCategoryEntity category = canteenCategoryMapper.selectById(detail.getCategoryId());
if (category != null && category.getCategoryType() != null) {
vo.setCategoryType(category.getCategoryType());
}
}
overlayDailyRemainOnAdVos(Collections.singletonList(vo), day, hourType, offline);
return vo;
}
/**
* 批量按「商户+日期+餐段+SPU」覆盖 AD/App 查看接口的 stock。
*/
private void overlayDailyRemainOnAdVos(List<ADCanteenSpuListVO> rows, String day, Integer hourType,
boolean offline) {
if (CollUtil.isEmpty(rows)) {
return;
}
java.time.LocalDate mealDate = parseMealDateOrToday(day);
List<Long> merchantIds = rows.stream().map(ADCanteenSpuListVO::getMerchantId).filter(Objects::nonNull)
.distinct().collect(Collectors.toList());
List<Long> spuIds = rows.stream().map(ADCanteenSpuListVO::getId).filter(Objects::nonNull).distinct()
.collect(Collectors.toList());
if (CollUtil.isEmpty(merchantIds) || CollUtil.isEmpty(spuIds)) {
return;
}
List<CanteenDailyMenuEntity> menus = canteenDailyMenuMapper.selectList(Wrappers.<CanteenDailyMenuEntity>lambdaQuery()
.in(CanteenDailyMenuEntity::getMerchantId, merchantIds)
.eq(CanteenDailyMenuEntity::getStartTime, mealDate)
.in(CanteenDailyMenuEntity::getSpuId, spuIds));
dailyMenuStockSupport.overlaySpuViewStock(rows, menus, hourType, offline);
}
private static java.time.LocalDate parseMealDateOrToday(String day) {
if (StrUtil.isBlank(day)) {
return java.time.LocalDate.now();
}
try {
return java.time.LocalDate.parse(day.trim());
}
catch (Exception e) {
return java.time.LocalDate.now();
}
}
@Override
public Object pageScope(Page page, CanteenSpuEntity canteenSpu) {
LambdaQueryWrapper<CanteenSpuEntity> wrapper = new LambdaQueryWrapper<>();
@@ -9,6 +9,8 @@
<result property="hourType" column="hour_type"/>
<result property="onlineStock" column="online_stock"/>
<result property="offlineStock" column="offline_stock"/>
<result property="onlineSold" column="online_sold"/>
<result property="offlineSold" column="offline_sold"/>
<result property="merchantId" column="merchant_id"/>
<result property="deptId" column="dept_id"/>
<result property="createBy" column="create_by"/>
@@ -36,6 +38,8 @@
<result property="stock" column="stock"/>
<result property="onlineStock" column="online_stock"/>
<result property="offlineStock" column="offline_stock"/>
<result property="onlineSold" column="online_sold"/>
<result property="offlineSold" column="offline_sold"/>
<result property="salesCount" column="sales_count"/>
<result property="hourType" column="hour_type"/>
<result property="sort" column="sort"/>
@@ -85,6 +89,8 @@
IFNULL(t.offline_stock, 600) AS stock,
IFNULL(t.online_stock, 600) AS online_stock,
IFNULL(t.offline_stock, 600) AS offline_stock,
IFNULL(t.online_sold, 0) AS online_sold,
IFNULL(t.offline_sold, 0) AS offline_sold,
sp.sales_count,
sp.sort,
sp.is_show,
@@ -133,6 +139,8 @@
IFNULL(t.offline_stock, 600) AS stock,
IFNULL(t.online_stock, 600) AS online_stock,
IFNULL(t.offline_stock, 600) AS offline_stock,
IFNULL(t.online_sold, 0) AS online_sold,
IFNULL(t.offline_sold, 0) AS offline_sold,
sp.sales_count,
sp.sort,
sp.is_show,
@@ -171,6 +179,8 @@
IFNULL(m.online_stock, 600) as stock,
IFNULL(m.online_stock, 600) as online_stock,
IFNULL(m.offline_stock, 600) as offline_stock,
IFNULL(m.online_sold, 0) as online_sold,
IFNULL(m.offline_sold, 0) as offline_sold,
s.pic_url,
s.sort,
s.is_show,
@@ -208,6 +218,8 @@
IFNULL(m.online_stock, 600) AS stock,
IFNULL(m.online_stock, 600) AS online_stock,
IFNULL(m.offline_stock, 600) AS offline_stock,
IFNULL(m.online_sold, 0) AS online_sold,
IFNULL(m.offline_sold, 0) AS offline_sold,
s.sales_count,
s.pic_url,
s.slider_pic_urls,
@@ -241,6 +253,8 @@
IFNULL(t.offline_stock, 600) AS stock,
IFNULL(t.online_stock, 600) AS online_stock,
IFNULL(t.offline_stock, 600) AS offline_stock,
IFNULL(t.online_sold, 0) AS online_sold,
IFNULL(t.offline_sold, 0) AS offline_sold,
sp.sort,
sp.description AS ingredients,
c.id AS category_id,
@@ -264,31 +278,38 @@
ORDER BY t.hour_type ASC, sp.sort ASC, t.id ASC
</select>
<!--
delta 与旧接口一致:下单扣减传负数、售后回传正数。
实际改写销量:sold = sold - delta(扣减时 sold 增加)。
expected 为操作前「剩余」快照(配额 - 销量)。
-->
<update id="updateOnlineStockDelta">
UPDATE canteen_daily_menu
SET online_stock = online_stock + #{delta}
SET online_sold = IFNULL(online_sold, 0) - #{delta}
WHERE del_flag = '0'
AND merchant_id = #{merchantId}
AND start_time = #{mealDate}
AND hour_type = #{hourType}
AND spu_id = #{spuId}
AND online_stock + #{delta} &gt;= 0
AND IFNULL(online_sold, 0) - #{delta} &gt;= 0
AND IFNULL(online_sold, 0) - #{delta} &lt;= IFNULL(online_stock, 600)
<if test="expected != null">
AND online_stock = #{expected}
AND (IFNULL(online_stock, 600) - IFNULL(online_sold, 0)) = #{expected}
</if>
</update>
<update id="updateOfflineStockDelta">
UPDATE canteen_daily_menu
SET offline_stock = offline_stock + #{delta}
SET offline_sold = IFNULL(offline_sold, 0) - #{delta}
WHERE del_flag = '0'
AND merchant_id = #{merchantId}
AND start_time = #{mealDate}
AND hour_type = #{hourType}
AND spu_id = #{spuId}
AND offline_stock + #{delta} &gt;= 0
AND IFNULL(offline_sold, 0) - #{delta} &gt;= 0
AND IFNULL(offline_sold, 0) - #{delta} &lt;= IFNULL(offline_stock, 600)
<if test="expected != null">
AND offline_stock = #{expected}
AND (IFNULL(offline_stock, 600) - IFNULL(offline_sold, 0)) = #{expected}
</if>
</update>
</mapper>
@@ -68,6 +68,11 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn
*/
private String productName;
/**
* 购物车商品 SPU ID;非空时只返回购物车含该商品的订单(每日菜谱「谁订了这道菜」)。
*/
private Long productId;
private LocalDateTime[] refundTimeList;
/** 预计使用日期范围。 */
@@ -2222,9 +2222,10 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
for (Map.Entry<Long, Integer> entry : required.entrySet()) {
DailyMenuAppVO row = menuBySpu.get(entry.getKey());
int onlineRemain = row.getOnlineStock() == null
? (row.getStock() == null ? 0 : row.getStock())
: row.getOnlineStock();
// 线上剩余:优先 remainOnlineStock;兼容旧字段 stock/onlineStock(历史把剩余写在配额字段)
int onlineRemain = row.getRemainOnlineStock() != null ? row.getRemainOnlineStock()
: (row.getStock() != null ? row.getStock()
: (row.getOnlineStock() == null ? 0 : row.getOnlineStock()));
if (onlineRemain < entry.getValue()) {
throw new I18nBizException(ErrorCodes.HARD_PRODUCT_STOCK_LOW, row.getName());
}
@@ -2742,7 +2743,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
/**
* 按每日菜谱批次扣减/回滚「线上」库存(canteen_daily_menu.online_stock)。
* 按每日菜谱批次扣减/回滚「线上」净销量(canteen_daily_menu.online_sold;配额 online_stock 不动)。
* 安卓机线下通道走 fastenerInventory → OFFLINE,互不影响。
*/
private void updateDailyMenuReserveInventory(List<DailyMenuReserveGroupContext> contexts, int dinerCount,
@@ -4666,8 +4667,9 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
if (row == null) {
throw new I18nBizException(ErrorCodes.HARD_CANTEEN_SPU_BAD);
}
int offlineRemain = row.getOfflineStock() != null ? row.getOfflineStock()
: (row.getStock() == null ? 0 : row.getStock());
int offlineRemain = row.getRemainOfflineStock() != null ? row.getRemainOfflineStock()
: (row.getStock() != null ? row.getStock()
: (row.getOfflineStock() != null ? row.getOfflineStock() : 0));
if (entry.getValue() < 0 && offlineRemain < Math.abs(entry.getValue())) {
throw new I18nBizException(ErrorCodes.HARD_PRODUCT_STOCK_LOW, row.getName());
}
@@ -235,6 +235,14 @@
<if test="dto.apptUseDateTimeList != null and dto.apptUseDateTimeList.length == 2">
and a.appt_use_date between #{dto.apptUseDateTimeList[0]} and #{dto.apptUseDateTimeList[1]}
</if>
<if test="dto.productId != null">
and exists (
select 1 from hard_car c
where c.parent_car_id = a.parent_car_id
and c.del_flag = '0'
and c.product_id = #{dto.productId}
)
</if>
<if test="dto.apptUseDateStart != null">
and a.appt_use_date &gt;= #{dto.apptUseDateStart}
</if>