|
|
@@ -23,6 +23,7 @@ import com.fs.company.domain.CompanyUser;
|
|
|
import com.fs.company.mapper.CompanyUserMapper;
|
|
|
import com.fs.core.utils.OrderCodeUtils;
|
|
|
import com.fs.his.domain.*;
|
|
|
+import com.fs.his.dto.ComputeIntegralOrderMoneyDTO;
|
|
|
import com.fs.his.enums.BusinessTypeEnum;
|
|
|
import com.fs.his.enums.FsUserIntegralLogTypeEnum;
|
|
|
import com.fs.his.enums.PaymentMethodEnum;
|
|
|
@@ -32,17 +33,28 @@ import com.fs.his.service.IFsIntegralCartService;
|
|
|
import com.fs.his.service.IFsIntegralOrderService;
|
|
|
import com.fs.his.service.IFsStorePaymentService;
|
|
|
import com.fs.his.service.IFsUserIntegralLogsService;
|
|
|
+import com.fs.his.service.IFsUserService;
|
|
|
import com.fs.his.utils.PhoneUtil;
|
|
|
import com.fs.his.vo.FsIntegralOrderListUVO;
|
|
|
import com.fs.his.vo.FsIntegralOrderListVO;
|
|
|
import com.fs.his.vo.FsIntegralOrderPVO;
|
|
|
import com.fs.his.vo.FsStoreProductDeliverExcelVO;
|
|
|
+import com.fs.hisStore.domain.FsShippingTemplatesFreeScrm;
|
|
|
+import com.fs.hisStore.domain.FsShippingTemplatesRegionScrm;
|
|
|
+import com.fs.hisStore.domain.FsShippingTemplatesScrm;
|
|
|
+import com.fs.hisStore.enums.ShippingTempEnum;
|
|
|
+import com.fs.hisStore.mapper.FsShippingTemplatesFreeScrmMapper;
|
|
|
+import com.fs.hisStore.mapper.FsShippingTemplatesRegionScrmMapper;
|
|
|
+import com.fs.hisStore.mapper.FsShippingTemplatesScrmMapper;
|
|
|
import com.fs.qw.domain.QwUser;
|
|
|
import com.fs.qw.mapper.QwUserMapper;
|
|
|
import com.fs.tzBankPay.doman.PayType;
|
|
|
import com.fs.ybPay.domain.OrderResult;
|
|
|
import com.fs.ybPay.dto.OrderQueryDTO;
|
|
|
+import com.fs.ybPay.dto.YopRefundRequestDTO;
|
|
|
+import com.fs.ybPay.dto.YopRefundResponseDTO;
|
|
|
import com.fs.ybPay.service.IPayService;
|
|
|
+import com.fs.ybPay.service.IYopPayService;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.collections.CollectionUtils;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
@@ -52,9 +64,11 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.util.*;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
* 积分商品订单Service业务层处理
|
|
|
@@ -66,12 +80,30 @@ import java.util.concurrent.TimeUnit;
|
|
|
@Service
|
|
|
public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
{
|
|
|
+ /**
|
|
|
+ * 限购缓存 Redis Key 前缀
|
|
|
+ * <p>完整格式:integral:purchase:count:{userId}:{goodsId},缓存用户已购某商品的有效订单数</p>
|
|
|
+ */
|
|
|
+ private static final String REDIS_KEY_INTEGRAL_PURCHASE_COUNT = "integral:purchase:count:";
|
|
|
+
|
|
|
+ /** 限购缓存过期时间(1小时),过期后回源数据库重建 */
|
|
|
+ private static final Integer PURCHASE_COUNT_CACHE_TTL = 1;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private FsCouponMapper fsCouponMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private FsUserCouponMapper fsUserCouponMapper;
|
|
|
+
|
|
|
@Autowired
|
|
|
private FsIntegralOrderMapper fsIntegralOrderMapper;
|
|
|
|
|
|
@Autowired
|
|
|
private FsUserAddressMapper fsUserAddressMapper;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private FsCityMapper fsCityMapper;
|
|
|
+
|
|
|
@Autowired
|
|
|
private FsUserMapper fsUserMapper;
|
|
|
|
|
|
@@ -80,6 +112,15 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
@Autowired
|
|
|
private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private FsShippingTemplatesScrmMapper fsShippingTemplatesScrmMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private FsShippingTemplatesFreeScrmMapper fsShippingTemplatesFreeScrmMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private FsShippingTemplatesRegionScrmMapper fsShippingTemplatesRegionScrmMapper;
|
|
|
+
|
|
|
@Autowired
|
|
|
private QwUserMapper qwUserMapper;
|
|
|
|
|
|
@@ -91,10 +132,25 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
private RedisCache redisCache;
|
|
|
@Autowired
|
|
|
private IPayService ybPayService;
|
|
|
+ /**
|
|
|
+ * 易宝聚合支付SDK(退款用,替代旧版 ybPayService.refund + MD5签名方式)
|
|
|
+ */
|
|
|
+ @Autowired
|
|
|
+ private IYopPayService yopPayService;
|
|
|
@Autowired
|
|
|
private IFsStorePaymentService storePaymentService;
|
|
|
+ /**
|
|
|
+ * 支付明细Mapper(查询订单关联的支付记录,用于退款)
|
|
|
+ */
|
|
|
+ @Autowired
|
|
|
+ private FsStorePaymentMapper fsStorePaymentMapper;
|
|
|
@Autowired
|
|
|
private IFsIntegralCartService cartService;
|
|
|
+ /**
|
|
|
+ * 用户积分Service(提供原子扣减/增加积分能力,避免应用层"读-改-写"并发超扣)
|
|
|
+ */
|
|
|
+ @Autowired
|
|
|
+ private IFsUserService userService;
|
|
|
|
|
|
/**
|
|
|
* 查询积分商品订单
|
|
|
@@ -203,7 +259,7 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- @Transactional
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public R createOrder(FsIntegralOrderCreateParam param) {
|
|
|
|
|
|
FsUser user=fsUserMapper.selectFsUserByUserId(param.getUserId());
|
|
|
@@ -225,6 +281,14 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
return R.error("积分不足");
|
|
|
}
|
|
|
|
|
|
+ // 限购校验:limitNum > 0 表示开启限购,校验用户已购数量是否已达上限
|
|
|
+ if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
|
|
|
+ int purchasedCount = getUserPurchasedCount(param.getUserId(), param.getGoodsId());
|
|
|
+ if (purchasedCount + 1 > integralGoods.getLimitNum()) {
|
|
|
+ return R.error("超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 减库存
|
|
|
if (fsIntegralGoodsMapper.subStock(integralGoods.getGoodsId(), 1) <= 0) {
|
|
|
throw new CustomException("库存不足");
|
|
|
@@ -298,22 +362,25 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
|
|
|
if(fsIntegralOrderMapper.insertFsIntegralOrder(order)>0){
|
|
|
if (order.getPayType() != 2) {
|
|
|
- //写入日志
|
|
|
- FsUser userMap=new FsUser();
|
|
|
- userMap.setUserId(user.getUserId());
|
|
|
- userMap.setIntegral(user.getIntegral()-totalIntegral);
|
|
|
- fsUserMapper.updateFsUser(userMap);
|
|
|
+ // 原子扣减用户积分(SQL 层 integral = integral - ? where integral >= ?,并发安全)
|
|
|
+ // 参考 ylrz 商城订单 FsStoreOrderScrmServiceImpl.decIntegral 规范实现
|
|
|
+ userService.decIntegral(order.getUserId(), totalIntegral.doubleValue());
|
|
|
+ // 重新查询扣减后余额,用于记录 balance
|
|
|
+ FsUser userAfter = fsUserMapper.selectFsUserByUserId(order.getUserId());
|
|
|
FsUserIntegralLogs logs = new FsUserIntegralLogs();
|
|
|
logs.setIntegral(-totalIntegral);
|
|
|
logs.setUserId(order.getUserId());
|
|
|
- logs.setBalance(userMap.getIntegral());
|
|
|
+ logs.setBalance(userAfter.getIntegral());
|
|
|
logs.setLogType(5);
|
|
|
logs.setBusinessId(order.getOrderId().toString());
|
|
|
logs.setCreateTime(new Date());
|
|
|
- int shard = ShardingUtil.shardOf(order.getUserId());
|
|
|
- fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs, shard);
|
|
|
+ // 独立事务写入分表日志,与订单主事务解耦
|
|
|
+ userIntegralLogsService.insertLog(logs);
|
|
|
}
|
|
|
|
|
|
+ // 失效限购缓存,下次查询时从数据库重建(保证数据一致性)
|
|
|
+ invalidatePurchaseCountCache(order.getUserId(), goodsItem.get(0).getGoodsId());
|
|
|
+
|
|
|
// 积分支付
|
|
|
if (order.getPayType() == 1) {
|
|
|
// 首次完成积分商城下单
|
|
|
@@ -337,6 +404,48 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 获取用户已购买指定积分商品的有效订单数(限购校验用)
|
|
|
+ * <p>缓存策略:优先查 Redis,未命中则回源数据库并回填缓存(1小时过期)。
|
|
|
+ * 缓存过期后会重新从数据库查询,保证用户取消订单后限购数量能及时更新。</p>
|
|
|
+ *
|
|
|
+ * @param userId 用户ID
|
|
|
+ * @param goodsId 积分商品ID
|
|
|
+ * @return 已购买有效订单数(非负)
|
|
|
+ */
|
|
|
+ private int getUserPurchasedCount(Long userId, Long goodsId) {
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
|
|
|
+ Integer count = redisCache.getCacheObject(redisKey);
|
|
|
+ if (count != null) {
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+ // 回源数据库查询有效订单数(排除已取消和已删除)
|
|
|
+ count = fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
|
|
|
+ if (count < 0) {
|
|
|
+ count = 0;
|
|
|
+ }
|
|
|
+ // 回填 Redis 缓存,1小时过期
|
|
|
+ redisCache.setCacheObject(redisKey, count, PURCHASE_COUNT_CACHE_TTL, TimeUnit.HOURS);
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 失效用户购买指定商品的限购缓存
|
|
|
+ * <p>订单创建成功后调用,确保下次限购校验时从数据库重新加载最新数据。</p>
|
|
|
+ *
|
|
|
+ * @param userId 用户ID
|
|
|
+ * @param goodsId 积分商品ID
|
|
|
+ */
|
|
|
+ private void invalidatePurchaseCountCache(Long userId, Long goodsId) {
|
|
|
+ try {
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
|
|
|
+ redisCache.deleteObject(redisKey);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 缓存失效失败不影响主流程,下次限购校验时缓存过期后会自动重建
|
|
|
+ log.warn("失效限购缓存失败, userId={}, goodsId={}", userId, goodsId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 创建购物车订单
|
|
|
* @param param 参数
|
|
|
@@ -516,15 +625,14 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
|| order.getPayMoney().compareTo(BigDecimal.ZERO) <= 0){
|
|
|
return R.error("非法操作");
|
|
|
}
|
|
|
-
|
|
|
- PayOrderParam payOrderParam = buildPayOrderParam(paymentMethod, order);
|
|
|
+ PayOrderParam payOrderParam = buildPayOrderParam(paymentMethod, order,param);
|
|
|
return storePaymentService.processPayment(payOrderParam);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 构建参数
|
|
|
*/
|
|
|
- private static PayOrderParam buildPayOrderParam(PaymentMethodEnum paymentMethod, FsIntegralOrder order) {
|
|
|
+ private static PayOrderParam buildPayOrderParam(PaymentMethodEnum paymentMethod, FsIntegralOrder order,FsIntegralOrderDoPayParam param) {
|
|
|
PayOrderParam payOrderParam = new PayOrderParam();
|
|
|
payOrderParam.setOrderId(order.getOrderId());
|
|
|
payOrderParam.setOrderCode(order.getOrderCode());
|
|
|
@@ -534,6 +642,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
payOrderParam.setCompanyUserId(order.getCompanyUserId());
|
|
|
payOrderParam.setPaymentMethod(paymentMethod);
|
|
|
payOrderParam.setBusinessType(BusinessTypeEnum.INTEGRAL_ORDER);
|
|
|
+ if(StringUtils.isNotBlank(param.getAppId())){
|
|
|
+ payOrderParam.setAppId(param.getAppId());
|
|
|
+ }
|
|
|
return payOrderParam;
|
|
|
}
|
|
|
|
|
|
@@ -602,6 +713,91 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
return R.ok();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 积分订单支付回调(易宝聚合支付SDK版)
|
|
|
+ * <p>由 PayScrmController.multiStoreYbAggPayNotify 路由调用,参数直接取自易宝回调明文。</p>
|
|
|
+ * <p>幂等设计:FsStorePayment.status==0 且 FsIntegralOrder.status==4 才执行更新,
|
|
|
+ * 重复回调不会重复处理。Controller 层已按 payCode 加 Redisson 分布式锁。</p>
|
|
|
+ *
|
|
|
+ * @return "success"=处理成功 / "FAIL"=处理失败需重试 / ""=异常回滚或幂等已处理
|
|
|
+ */
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ @Override
|
|
|
+ public String payConfirmByYop(String payCode, String uniqueOrderNo, String channelTrxId, String bankOrderId) {
|
|
|
+ log.info("进入积分商城订单支付回调(Yop) payCode: {}, uniqueOrderNo: {}, channelTrxId: {}, bankOrderId: {}",
|
|
|
+ payCode, uniqueOrderNo, channelTrxId, bankOrderId);
|
|
|
+
|
|
|
+ if (StringUtils.isEmpty(payCode)) {
|
|
|
+ log.error("积分订单回调(Yop) payCode 为空");
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 查询支付记录
|
|
|
+ FsStorePayment storePayment = storePaymentService.selectFsStorePaymentByPaymentCode(payCode);
|
|
|
+ if (Objects.isNull(storePayment)) {
|
|
|
+ log.error("积分订单回调(Yop) 支付单号不存在 payCode: {}", payCode);
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 幂等校验:支付记录已处理则直接返回成功(易宝重试场景)
|
|
|
+ if (storePayment.getStatus() != null && storePayment.getStatus() == 1) {
|
|
|
+ log.info("积分订单回调(Yop) 支付记录已处理,幂等返回 success payCode: {}", payCode);
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 更新支付记录(status=1 已支付,回写易宝交易号)
|
|
|
+ FsStorePayment paymentUpdate = new FsStorePayment();
|
|
|
+ paymentUpdate.setPaymentId(storePayment.getPaymentId());
|
|
|
+ paymentUpdate.setStatus(1);
|
|
|
+ paymentUpdate.setPayTime(new Date());
|
|
|
+ paymentUpdate.setTradeNo(uniqueOrderNo);
|
|
|
+ paymentUpdate.setBankSerialNo(bankOrderId);
|
|
|
+ paymentUpdate.setBankTransactionId(channelTrxId);
|
|
|
+ // 当前 createYbPayment 仅支持微信小程序支付,回调 payTypeCode 固定为微信小程序
|
|
|
+ paymentUpdate.setPayTypeCode(PayType.WECHAT_MINI_PROGRAM_PAYMENT.name());
|
|
|
+ storePaymentService.updateFsStorePayment(paymentUpdate);
|
|
|
+ log.info("积分订单回调(Yop) 更新支付记录完成 paymentId: {}, tradeNo: {}", storePayment.getPaymentId(), uniqueOrderNo);
|
|
|
+
|
|
|
+ // 4. 查询积分订单
|
|
|
+ FsIntegralOrder order;
|
|
|
+ try {
|
|
|
+ order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderId(Long.parseLong(storePayment.getBusinessId()));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.error("积分订单回调(Yop) businessId 非法 paymentId: {}, businessId: {}", storePayment.getPaymentId(), storePayment.getBusinessId());
|
|
|
+ throw new CustomException("支付记录关联订单ID非法");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Objects.isNull(order)) {
|
|
|
+ log.error("积分订单回调(Yop) 积分订单不存在 orderId: {}", storePayment.getBusinessId());
|
|
|
+ throw new CustomException("积分订单不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 幂等校验:订单状态非待支付(4)则不重复更新(支付记录已更新,但订单可能已被其他流程处理)
|
|
|
+ if (order.getStatus() == null || order.getStatus() != 4) {
|
|
|
+ log.warn("积分订单回调(Yop) 订单状态非待支付,跳过订单更新 orderId: {}, status: {}", order.getOrderId(), order.getStatus());
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 更新积分订单:status=1 已支付,isPay=1
|
|
|
+ FsIntegralOrder orderUpdate = new FsIntegralOrder();
|
|
|
+ orderUpdate.setOrderId(order.getOrderId());
|
|
|
+ orderUpdate.setIsPay(1);
|
|
|
+ orderUpdate.setStatus(1);
|
|
|
+ orderUpdate.setPayTime(LocalDateTime.now());
|
|
|
+ fsIntegralOrderMapper.updateFsIntegralOrder(orderUpdate);
|
|
|
+ log.info("积分订单回调(Yop) 更新积分订单完成 orderId: {}, status: 4->1", order.getOrderId());
|
|
|
+
|
|
|
+ // 7. 首次完成积分商城下单钩子(与旧版 payConfirm 保持功能对齐,当前未启用)
|
|
|
+ // 未来开启"购物送积分"时取消注释即可,无需再找代码位置
|
|
|
+ FsUserAddIntegralTemplateParam integralParam = new FsUserAddIntegralTemplateParam();
|
|
|
+ integralParam.setUserId(order.getUserId());
|
|
|
+ integralParam.setLogType(FsUserIntegralLogTypeEnum.TYPE_22.getValue());
|
|
|
+ integralParam.setBusinessId(order.getOrderId().toString());
|
|
|
+// userIntegralLogsService.addIntegralTemplate(integralParam);
|
|
|
+
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public AjaxResult export(FsIntegralOrder fsIntegralOrder) {
|
|
|
List<FsIntegralOrder> list = fsIntegralOrderMapper.selectFsIntegralOrderList(fsIntegralOrder);
|
|
|
@@ -654,6 +850,200 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
return i;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 管理员强制退款(不校验是否发货)
|
|
|
+ * <p>流程:
|
|
|
+ * <ol>
|
|
|
+ * <li>更新订单状态为已取消(status=-1)</li>
|
|
|
+ * <li>还原商品库存</li>
|
|
|
+ * <li>易宝原路退款(Yop SDK,退款处理中也算成功,等回调最终确认)</li>
|
|
|
+ * <li>原子退回用户积分(incIntegral)+ 独立事务写积分日志(logType=27,REQUIRES_NEW)</li>
|
|
|
+ * </ol>
|
|
|
+ * </p>
|
|
|
+ * <p>幂等:通过 logType=27 存在性判断,重复退款不会重复退积分。</p>
|
|
|
+ * <p>事务隔离:资金退款在主事务,积分退回异常不回滚退款事务(记日志人工补偿,符合项目规范)。</p>
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public int mandatoryRefunds(String orderCode) {
|
|
|
+ FsIntegralOrder order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderCode(orderCode);
|
|
|
+ if (null == order) {
|
|
|
+ throw new ServiceException("订单不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 修改订单状态为已取消
|
|
|
+ int rows = fsIntegralOrderMapper.cancelOrder(order.getOrderId());
|
|
|
+ if (rows <= 0) {
|
|
|
+ throw new ServiceException("订单状态更新失败,可能已被处理");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 还原库存
|
|
|
+ restoreStock(order);
|
|
|
+
|
|
|
+ // 3. 易宝原路退款(仅纯现金/积分+现金订单需要退资金)
|
|
|
+ refundPaymentIfNeeded(order);
|
|
|
+
|
|
|
+ // 4. 退回积分(独立事务,异常不回滚主事务)
|
|
|
+ refundIntegralSafely(order);
|
|
|
+
|
|
|
+ return rows;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 确认收货(status: 2 待收货 → 3 已完成)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public int finishOrder(String orderCode) {
|
|
|
+ FsIntegralOrder order = fsIntegralOrderMapper.selectFsIntegralOrderByOrderCode(orderCode);
|
|
|
+ if (null == order) {
|
|
|
+ throw new ServiceException("订单不存在");
|
|
|
+ }
|
|
|
+ if (order.getStatus() == null || order.getStatus() != 2) {
|
|
|
+ throw new ServiceException("订单未发货或已完成,无法确认收货");
|
|
|
+ }
|
|
|
+ // 带旧状态校验的乐观更新,避免并发覆盖
|
|
|
+ int rows = fsIntegralOrderMapper.finishOrder(order.getOrderId(), order.getStatus());
|
|
|
+ if (rows <= 0) {
|
|
|
+ throw new ServiceException("订单状态已变更,请刷新后重试");
|
|
|
+ }
|
|
|
+ return rows;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 还原商品库存
|
|
|
+ * <p>兼容 item_json 为单对象(旧数据)和数组(新数据)两种格式。</p>
|
|
|
+ */
|
|
|
+ private void restoreStock(FsIntegralOrder order) {
|
|
|
+ String itemJson = order.getItemJson();
|
|
|
+ if (StringUtils.isEmpty(itemJson)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (itemJson.startsWith("[") && itemJson.endsWith("]")) {
|
|
|
+ List<FsIntegralGoods> goodsItem = JSONUtil.toBean(itemJson, new TypeReference<List<FsIntegralGoods>>(){}, true);
|
|
|
+ goodsItem.forEach(goods -> fsIntegralGoodsMapper.addStock(goods.getGoodsId(), Objects.isNull(goods.getNum()) ? 1 : goods.getNum()));
|
|
|
+ } else {
|
|
|
+ FsIntegralGoods integralGoods = JSONUtil.toBean(itemJson, FsIntegralGoods.class);
|
|
|
+ fsIntegralGoodsMapper.addStock(integralGoods.getGoodsId(), Objects.isNull(integralGoods.getNum()) ? 1 : integralGoods.getNum());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 易宝原路退款(仅当订单有支付记录时执行)
|
|
|
+ * <p>退款成功(status=SUCCESS)或处理中(status=PROCESSING)都更新支付记录,
|
|
|
+ * 退款失败抛异常回滚主事务(订单状态还原)。</p>
|
|
|
+ */
|
|
|
+ private void refundPaymentIfNeeded(FsIntegralOrder order) {
|
|
|
+ // 查询订单关联的支付记录(businessType=6 积分订单)
|
|
|
+ List<FsStorePayment> payments = fsStorePaymentMapper.selectFsStorePaymentByPay(
|
|
|
+ BusinessTypeEnum.INTEGRAL_ORDER.getCode(), order.getOrderId());
|
|
|
+ if (payments == null || payments.isEmpty()) {
|
|
|
+ // 纯积分订单无支付记录,跳过资金退款
|
|
|
+ log.info("积分订单强制退款:无支付记录,跳过资金退款 orderId: {}", order.getOrderId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (payments.size() > 1) {
|
|
|
+ log.warn("积分订单强制退款:存在多笔支付记录,仅处理第一笔 orderId: {}, 支付记录数: {}",
|
|
|
+ order.getOrderId(), payments.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ FsStorePayment payment = payments.get(0);
|
|
|
+ // 幂等校验:支付记录已退款则跳过
|
|
|
+ if (payment.getStatus() != null && payment.getStatus() < 0) {
|
|
|
+ log.info("积分订单强制退款:支付记录已退款,跳过 paymentId: {}, status: {}", payment.getPaymentId(), payment.getStatus());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建易宝退款请求(对齐 ylrz 商城订单 FsStoreAfterSalesScrmServiceImpl 退款逻辑)
|
|
|
+ YopRefundRequestDTO yopRefundRequest = new YopRefundRequestDTO();
|
|
|
+ yopRefundRequest.setOrderId(BusinessTypeEnum.INTEGRAL_ORDER.getPrefix() + "-" + payment.getPayCode());
|
|
|
+ yopRefundRequest.setUniqueOrderNo(payment.getTradeNo());
|
|
|
+ yopRefundRequest.setRefundRequestId("refund-" + payment.getPayCode());
|
|
|
+ yopRefundRequest.setRefundAmount(payment.getPayMoney());
|
|
|
+ yopRefundRequest.setDescription("积分订单管理员强制退款");
|
|
|
+
|
|
|
+ YopRefundResponseDTO yopRefundResult = yopPayService.refund(yopRefundRequest);
|
|
|
+ log.info("易宝SDK退款返回结果: 积分订单orderId: {}, code: {}, status: {}",
|
|
|
+ order.getOrderId(), yopRefundResult.getCode(), yopRefundResult.getStatus());
|
|
|
+
|
|
|
+ if (!yopRefundResult.isAccepted()) {
|
|
|
+ throw new CustomException("易宝退款请求失败:" + yopRefundResult.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新支付记录(最小更新对象,避免覆盖其他字段)
|
|
|
+ FsStorePayment paymentUpdate = new FsStorePayment();
|
|
|
+ paymentUpdate.setPaymentId(payment.getPaymentId());
|
|
|
+ paymentUpdate.setRefundMoney(payment.getPayMoney());
|
|
|
+ if (yopRefundResult.isRefundSuccess()) {
|
|
|
+ // 退款立即成功 → 标记已退款
|
|
|
+ paymentUpdate.setStatus(-1);
|
|
|
+ paymentUpdate.setRefundTime(new Date());
|
|
|
+ } else if (yopRefundResult.isProcessing()) {
|
|
|
+ // 退款处理中 → 标记退款中(status=-2),等待退款回调最终确认
|
|
|
+ paymentUpdate.setStatus(-2);
|
|
|
+ } else {
|
|
|
+ // 其他状态(CANCEL/SUSPEND等)视为退款失败
|
|
|
+ throw new CustomException("易宝退款异常,状态:" + yopRefundResult.getStatus());
|
|
|
+ }
|
|
|
+ storePaymentService.updateFsStorePayment(paymentUpdate);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 退回用户积分(独立事务,异常不回滚主事务)
|
|
|
+ * <p>幂等:通过 logType=27(退款订单退回积分)存在性判断,已退过则跳过。</p>
|
|
|
+ * <p>事务隔离:userIntegralLogsService.insertLog 自带 REQUIRES_NEW,
|
|
|
+ * incIntegral 无事务注解(裸执行),异常时 try-catch 记日志,不抛出。</p>
|
|
|
+ */
|
|
|
+ private void refundIntegralSafely(FsIntegralOrder order) {
|
|
|
+ if (StringUtils.isEmpty(order.getIntegral())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ long refundIntegral;
|
|
|
+ try {
|
|
|
+ refundIntegral = Long.parseLong(order.getIntegral());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.error("积分订单退款:积分字段非法 orderId: {}, integral: {}", order.getOrderId(), order.getIntegral(), e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (refundIntegral <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 幂等校验:检查是否已存在 logType=27 的退款记录
|
|
|
+ String businessId = order.getOrderId().toString();
|
|
|
+ List<FsUserIntegralLogs> existLogs = userIntegralLogsService.selectByUserIdAndBusinessIdAll(order.getUserId(), businessId);
|
|
|
+ boolean hasRefundRecord = existLogs != null && existLogs.stream()
|
|
|
+ .anyMatch(l -> FsUserIntegralLogTypeEnum.TYPE_27.getValue().equals(l.getLogType()));
|
|
|
+ if (hasRefundRecord) {
|
|
|
+ log.info("积分订单退款:已存在logType=27退款记录,跳过退积分 orderId: {}", order.getOrderId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 原子加回积分(SQL 层 integral = integral + ?,并发安全,与下单时 decIntegral 原子扣减对称)
|
|
|
+ userService.incIntegral(order.getUserId(), refundIntegral);
|
|
|
+ // 重新查询加回后余额,用于记录 balance
|
|
|
+ FsUser userAfter = fsUserMapper.selectFsUserByUserId(order.getUserId());
|
|
|
+ FsUserIntegralLogs logs = new FsUserIntegralLogs();
|
|
|
+ logs.setIntegral(refundIntegral);
|
|
|
+ logs.setUserId(order.getUserId());
|
|
|
+ logs.setBalance(userAfter.getIntegral());
|
|
|
+ // logType=27 退款订单退回积分(区别于 logType=26 取消订单退回积分)
|
|
|
+ logs.setLogType(FsUserIntegralLogTypeEnum.TYPE_27.getValue());
|
|
|
+ logs.setBusinessId(businessId);
|
|
|
+ logs.setBusinessType(2);
|
|
|
+ logs.setStatus(0);
|
|
|
+ logs.setCreateTime(new Date());
|
|
|
+ // 独立事务写入分表日志(REQUIRES_NEW),与退款主事务解耦
|
|
|
+ userIntegralLogsService.insertLog(logs);
|
|
|
+ log.info("积分订单退款:退回积分成功 orderId: {}, refundIntegral: {}", order.getOrderId(), refundIntegral);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 积分退回失败不回滚退款主事务,记录错误日志供人工补偿
|
|
|
+ log.error("积分订单退款:退回积分异常,需人工补偿 orderId: {}, refundIntegral: {}",
|
|
|
+ order.getOrderId(), refundIntegral, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 处理手机号脱敏
|
|
|
*/
|
|
|
@@ -713,4 +1103,235 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
BeanUtils.copyProperties(original, cloned);
|
|
|
return cloned;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 积分订单支付金额计算
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public R computeOrder(FsIntegralOrderComputeParam param) {
|
|
|
+ // 查询用户,判断用户是否存在
|
|
|
+ FsUser fsUser = fsUserMapper.selectFsUserById(param.getUserId());
|
|
|
+ if (fsUser == null || fsUser.getIsDel() == 1) {
|
|
|
+ return R.error("用户不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (param.getGoodsId() == null && (param.getCartIds() == null || param.getCartIds().isEmpty())) {
|
|
|
+ return R.error("商品不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ ComputeIntegralOrderMoneyDTO moneyDTO = new ComputeIntegralOrderMoneyDTO();
|
|
|
+ List<FsIntegralGoods> goodsItem = new ArrayList<>();
|
|
|
+
|
|
|
+ // 判断是购物车还是单商品
|
|
|
+ if (param.getGoodsId() != null) {
|
|
|
+ FsIntegralGoods fsIntegralGoods = fsIntegralGoodsMapper.selectFsIntegralGoodsByGoodsId(param.getGoodsId());
|
|
|
+ if (fsIntegralGoods == null) {
|
|
|
+ return R.error("积分商品不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fsIntegralGoods.getStatus() == 0) {
|
|
|
+ return R.error("积分商品已下架");
|
|
|
+ }
|
|
|
+
|
|
|
+ fsIntegralGoods.setNum(1);
|
|
|
+ goodsItem.add(fsIntegralGoods);
|
|
|
+
|
|
|
+ moneyDTO.setTotalCash(fsIntegralGoods.getCash());
|
|
|
+ moneyDTO.setCash(fsIntegralGoods.getCash());
|
|
|
+ moneyDTO.setTotalIntegral(new BigDecimal(fsIntegralGoods.getIntegral()));
|
|
|
+ moneyDTO.setIntegral(new BigDecimal(fsIntegralGoods.getIntegral()));
|
|
|
+
|
|
|
+ // 优惠金额计算
|
|
|
+ if (param.getUserCouponId() != null) {
|
|
|
+ applyCoupon(param.getUserCouponId(), fsUser.getUserId(), fsIntegralGoods, moneyDTO);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Wrapper<FsIntegralCart> wrapper = Wrappers.<FsIntegralCart>lambdaQuery().eq(FsIntegralCart::getUserId, fsUser.getUserId())
|
|
|
+ .in(FsIntegralCart::getId, param.getCartIds());
|
|
|
+ List<FsIntegralCart> carts = cartService.list(wrapper);
|
|
|
+ if (carts.isEmpty()) {
|
|
|
+ return R.error("购物车商品不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ for (FsIntegralCart cart : carts) {
|
|
|
+ FsIntegralGoods integralGoods = fsIntegralGoodsMapper.selectFsIntegralGoodsByGoodsId(cart.getGoodsId());
|
|
|
+ if (Objects.isNull(integralGoods)) {
|
|
|
+ throw new CustomException("商品不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算所需积分和现金
|
|
|
+ BigDecimal totalIntegral = new BigDecimal(integralGoods.getIntegral()).multiply(new BigDecimal(cart.getCartNum()));
|
|
|
+ BigDecimal totalCash = integralGoods.getCash().multiply(new BigDecimal(cart.getCartNum()));
|
|
|
+
|
|
|
+ moneyDTO.setTotalIntegral(moneyDTO.getTotalIntegral().add(totalIntegral));
|
|
|
+ moneyDTO.setTotalCash(moneyDTO.getTotalCash().add(totalCash));
|
|
|
+
|
|
|
+ integralGoods.setNum(cart.getCartNum());
|
|
|
+ goodsItem.add(integralGoods);
|
|
|
+ }
|
|
|
+
|
|
|
+ moneyDTO.setIntegral(moneyDTO.getTotalIntegral());
|
|
|
+ moneyDTO.setCash(moneyDTO.getTotalCash());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 运费计算
|
|
|
+ if (param.getUserAddressId() != null) {
|
|
|
+ FsUserAddress fsUserAddress = fsUserAddressMapper.selectFsUserAddressByAddressId(param.getUserAddressId());
|
|
|
+ if (fsUserAddress == null || fsUserAddress.getIsDel() == 1 || fsUserAddress.getIsConfirm() == 0
|
|
|
+ || !fsUser.getUserId().equals(fsUserAddress.getUserId())) {
|
|
|
+ return R.error("收货地址不存在");
|
|
|
+ }
|
|
|
+ calcDeliveryMoney(moneyDTO, fsUserAddress, goodsItem);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 返回扣减后的支付金额
|
|
|
+ return R.ok().put("data", moneyDTO);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算运费
|
|
|
+ */
|
|
|
+ private void calcDeliveryMoney(ComputeIntegralOrderMoneyDTO moneyDTO, FsUserAddress address, List<FsIntegralGoods> goodsItem) {
|
|
|
+ // 解析省市信息
|
|
|
+ FsCity province = fsCityMapper.selectByNameAndParentId(address.getProvince(), "0");
|
|
|
+ if (province == null) return;
|
|
|
+
|
|
|
+ FsCity city = fsCityMapper.selectByNameAndParentId(address.getCity(), province.getCityId());
|
|
|
+ if (city == null) return;
|
|
|
+
|
|
|
+ // 按运费模板分组(过滤无模板商品)
|
|
|
+ Map<Long, List<FsIntegralGoods>> templateGroupMap = goodsItem.stream()
|
|
|
+ .filter(goods -> goods.getTempId() != null)
|
|
|
+ .collect(Collectors.groupingBy(FsIntegralGoods::getTempId));
|
|
|
+
|
|
|
+ BigDecimal totalDeliveryMoney = BigDecimal.ZERO;
|
|
|
+
|
|
|
+ for (Map.Entry<Long, List<FsIntegralGoods>> entry : templateGroupMap.entrySet()) {
|
|
|
+ BigDecimal fee = calcTemplateFee(entry.getKey(), entry.getValue(), province, city);
|
|
|
+ totalDeliveryMoney = totalDeliveryMoney.add(fee);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新 DTO
|
|
|
+ moneyDTO.setDeliveryMoney(totalDeliveryMoney);
|
|
|
+ moneyDTO.setCash(moneyDTO.getCash().add(totalDeliveryMoney));
|
|
|
+ moneyDTO.setTotalCash(moneyDTO.getTotalCash().add(totalDeliveryMoney));
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算优惠
|
|
|
+ */
|
|
|
+ private void applyCoupon(Long userCouponId, Long userId, FsIntegralGoods integralGoods, ComputeIntegralOrderMoneyDTO moneyDTO) {
|
|
|
+ // 查询用户优惠券,判断是否有效,是否未使用
|
|
|
+ FsUserCoupon fsUserCoupon = fsUserCouponMapper.selectFsUserCouponById(userCouponId);
|
|
|
+ if (fsUserCoupon == null || !Objects.equals(fsUserCoupon.getUserId(), userId)) {
|
|
|
+ throw new CustomException("无效的优惠券");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fsUserCoupon.getStatus() == 1) {
|
|
|
+ throw new CustomException("优惠券已使用");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fsUserCoupon.getStatus() == 2 || (fsUserCoupon.getLimitTime() != null && fsUserCoupon.getLimitTime().before(new Date()))) {
|
|
|
+ throw new CustomException("优惠券已过期");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查看优惠券具体类型
|
|
|
+ FsCoupon fsCoupon = fsCouponMapper.selectFsCouponByCouponId(fsUserCoupon.getCouponId());
|
|
|
+ if (fsCoupon == null) {
|
|
|
+ throw new CustomException("无效的优惠券");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (fsCoupon.getCouponType() != 7 || !fsCoupon.getFreeGoodsId().equals(integralGoods.getGoodsId())) {
|
|
|
+ throw new CustomException("无效的优惠券");
|
|
|
+ }
|
|
|
+
|
|
|
+ moneyDTO.setDiscountIntegral(moneyDTO.getIntegral());
|
|
|
+ moneyDTO.setIntegral(BigDecimal.ZERO);
|
|
|
+ moneyDTO.setDiscountCash(moneyDTO.getCash());
|
|
|
+ moneyDTO.setCash(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算单个运费模板的运费
|
|
|
+ */
|
|
|
+ private BigDecimal calcTemplateFee(Long tempId, List<FsIntegralGoods> goodsList, FsCity province, FsCity city) {
|
|
|
+ FsShippingTemplatesScrm template = fsShippingTemplatesScrmMapper.selectFsShippingTemplatesById(tempId);
|
|
|
+
|
|
|
+ // 仅处理按件计费模板
|
|
|
+ if (template == null || !ShippingTempEnum.TYPE_1.getValue().equals(template.getType())) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 汇总商品总件数与总金额
|
|
|
+ int totalNum = 0;
|
|
|
+ BigDecimal totalPrice = BigDecimal.ZERO;
|
|
|
+ for (FsIntegralGoods goods : goodsList) {
|
|
|
+ int num = goods.getNum() != null ? goods.getNum() : 0;
|
|
|
+ if (num <= 0) continue;
|
|
|
+ totalNum += num;
|
|
|
+ if (goods.getCash() != null) {
|
|
|
+ totalPrice = totalPrice.add(goods.getCash().multiply(BigDecimal.valueOf(num)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (totalNum <= 0) return BigDecimal.ZERO;
|
|
|
+
|
|
|
+ // 检查指定包邮
|
|
|
+ if (isFreeShipping(template, province, city, totalNum, totalPrice)) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询运费区域配置(优先精确匹配,降级全国)
|
|
|
+ FsShippingTemplatesRegionScrm region = fsShippingTemplatesRegionScrmMapper.selectByTempIdAndProvinceIdAndCityId(template.getId(), province.getCityId(), city.getCityId());
|
|
|
+ if (region == null) {
|
|
|
+ region = fsShippingTemplatesRegionScrmMapper.selectByTempIdAndProvinceIdAndCityId(template.getId(), "0", null);
|
|
|
+ }
|
|
|
+
|
|
|
+ return region != null ? calcShippingFee(region, totalNum) : BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断是否满足包邮条件
|
|
|
+ */
|
|
|
+ private boolean isFreeShipping(FsShippingTemplatesScrm template, FsCity province, FsCity city, int totalNum, BigDecimal totalPrice) {
|
|
|
+ if (template.getAppoint() == null || template.getAppoint() != 1) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ FsShippingTemplatesFreeScrm freeRule = fsShippingTemplatesFreeScrmMapper
|
|
|
+ .selectByTempIdAndProvinceIdAndCityId(template.getId(), province.getCityId(), city.getCityId());
|
|
|
+ if (freeRule == null) return false;
|
|
|
+
|
|
|
+ // 件数满足包邮(0 表示不限件数)
|
|
|
+ boolean freeByNum = freeRule.getNumber() == null
|
|
|
+ || freeRule.getNumber().intValue() == 0
|
|
|
+ || totalNum >= freeRule.getNumber().intValue();
|
|
|
+
|
|
|
+ // 金额满足包邮(0 表示不限金额)
|
|
|
+ boolean freeByPrice = freeRule.getPrice() != null
|
|
|
+ && freeRule.getPrice().compareTo(BigDecimal.ZERO) > 0
|
|
|
+ && totalPrice.compareTo(freeRule.getPrice()) >= 0;
|
|
|
+
|
|
|
+ return freeByNum || freeByPrice; // 满足任一条件即可包邮
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据区域配置计算运费(按件计费)
|
|
|
+ */
|
|
|
+ private BigDecimal calcShippingFee(FsShippingTemplatesRegionScrm region, int totalNum) {
|
|
|
+ BigDecimal firstCount = region.getFirst();
|
|
|
+ BigDecimal firstPrice = region.getFirstPrice();
|
|
|
+ BigDecimal continueCount = region.getContinues();
|
|
|
+ BigDecimal continuePrice = region.getContinuePrice();
|
|
|
+
|
|
|
+ if (totalNum <= firstCount.intValue() || continueCount.compareTo(BigDecimal.ZERO) == 0) {
|
|
|
+ return firstPrice;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 超出首件部分,向上取整计算续件费用
|
|
|
+ BigDecimal extra = BigDecimal.valueOf(totalNum).subtract(firstCount);
|
|
|
+ BigDecimal continueUnits = extra.divide(continueCount, 0, RoundingMode.CEILING);
|
|
|
+ return firstPrice.add(continueUnits.multiply(continuePrice));
|
|
|
+ }
|
|
|
}
|