shinnosuke
2026-08-10 63093fb9b50e601808b0eafccb627a308fc16b29
fix(portal): 修复待支付订单原价展示
2个文件已修改
65 ■■■■■ 已修改文件
talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/PendingOrderPortalService.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
@@ -22,13 +22,19 @@
@Service
public class AppOrderPortalService {
    private final JdbcTemplate jdbc;
    private final PendingOrderPortalService pendingOrderPortalService;
    public AppOrderPortalService(JdbcTemplate jdbc) {
    public AppOrderPortalService(JdbcTemplate jdbc, PendingOrderPortalService pendingOrderPortalService) {
        this.jdbc = jdbc;
        this.pendingOrderPortalService = pendingOrderPortalService;
    }
    @Transactional
    public Map<String, Object> create(Long talentId, AppOrderCreateRequest request, String rawIdempotencyKey) {
        // A coupon is reserved only by an unpaid order.  Clear stale reservations
        // before checking/reusing an order so a user can buy again immediately
        // after the previous 15-minute payment countdown has elapsed.
        pendingOrderPortalService.expireDueOrders(talentId);
        Map<String, Object> talentCard = activePersonalCard(talentId);
        Map<String, Object> product = purchasableProduct(request.merchantId(), request.productId());
        int usedFreeCount = usedFreeCount(talentId, request.productId(), product.get("rightsConfig"), talentCard.get("cardLevel"));
@@ -39,6 +45,9 @@
        long originalAmount = MoneyUtils.yuanToCent(personalizedPrice.originalPrice());
        long basePayAmount = MoneyUtils.yuanToCent(personalizedPrice.salePrice());
        // Keep the original-price snapshot meaningful even when an old product
        // record has no original_price configured.
        originalAmount = Math.max(originalAmount, basePayAmount);
        long couponAmount = coupon == null ? 0 : Math.min(MoneyUtils.yuanToCent(decimal(coupon.get("couponAmount"))), basePayAmount);
        long payAmount = Math.max(0, basePayAmount - couponAmount);
        // 订单补贴金额需与待支付页保持一致:人才折扣与优惠券抵扣的合计。
talent-card-server/src/main/java/com/talentcard/server/business/portal/PendingOrderPortalService.java
@@ -30,12 +30,25 @@
    /** Cancels every unpaid order that has reached its payment deadline. */
    @Transactional
    public int expireDueOrders() {
        return jdbcTemplate.update("""
        return expireDueOrders(null);
    }
    /**
     * Cancels expired unpaid orders for one talent user.  Besides the scheduled
     * cleanup this is used on the purchase path, so an expired order can never
     * continue to occupy that user's coupon while the scheduler is waiting for
     * its next run.
     */
    @Transactional
    public int expireDueOrders(Long talentId) {
        String sql = """
            update benefit_order
            set order_status = 2
            where deleted = 0 and order_status = 0
              and expires_at is not null and expires_at <= current_timestamp
            """);
            """;
        if (talentId == null) return jdbcTemplate.update(sql);
        return jdbcTemplate.update(sql + " and talent_id = ?", talentId);
    }
    @Transactional
@@ -133,9 +146,10 @@
            select o.order_no, o.talent_id, o.merchant_id, o.order_type, o.product_id, o.order_amount, o.subsidy_amount, o.base_pay_amount,
                   o.pay_amount, o.coupon_amount, o.coupon_id, o.order_status, o.verify_status, o.expires_at, o.price_version,
                   o.merchant_name_snapshot, o.merchant_address_snapshot, o.merchant_mobile_snapshot,
                   o.merchant_longitude_snapshot, o.merchant_latitude_snapshot, o.product_name_snapshot,
                   c.coupon_name
                       o.merchant_longitude_snapshot, o.merchant_latitude_snapshot, o.product_name_snapshot,
                       c.coupon_name, c.coupon_amount as configured_coupon_amount, p.original_price as product_original_price
            from benefit_order o left join merchant_coupon c on c.id = o.coupon_id
            left join merchant_product p on p.id = o.product_id and p.deleted = 0
            where o.deleted = 0 and o.order_no = ? and o.talent_id = ?
            """, orderNo, talentId);
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在或已删除");
@@ -152,18 +166,42 @@
                && !"GROUP_BUY".equals(String.valueOf(order.get("orderType")))
                && orderStatus == 0 && !expired;
        result.put("couponSelectable", couponSelectable);
        result.put("couponLabel", order.get("couponName") == null ? "无" : "减" + yuan(order.get("couponAmount")).stripTrailingZeros().toPlainString() + "元");
        long couponAmount = longNumber(order.get("couponAmount"));
        if (couponAmount <= 0 && order.get("couponId") != null) {
            couponAmount = Math.min(
                    MoneyUtils.yuanToCent(decimal(order.get("configuredCouponAmount"))),
                    longNumber(order.get("basePayAmount")));
        }
        result.put("couponLabel", order.get("couponName") == null ? "无" : "减" + yuan(couponAmount).stripTrailingZeros().toPlainString() + "元");
        result.put("talentDiscountText", talentDiscountText(order));
        result.put("originalAmount", yuan(order.get("orderAmount")));
        long originalAmount = longNumber(order.get("orderAmount"));
        if (originalAmount <= 0) {
            BigDecimal productOriginalPrice = decimal(order.get("productOriginalPrice"));
            originalAmount = productOriginalPrice.signum() > 0
                    ? MoneyUtils.yuanToCent(productOriginalPrice)
                    : longNumber(order.get("basePayAmount"));
        }
        if (originalAmount <= 0) {
            // Historical orders may have missed both price snapshots.  Their selected
            // coupon and payable amount still represent the amount before coupon use.
            originalAmount = Math.max(0, longNumber(order.get("payAmount")) + couponAmount);
        }
        result.put("originalAmount", yuan(originalAmount));
        // “优惠金额”在待支付页仅表示优惠券抵扣,不应把人才折扣或补贴混入其中。
        result.put("discountAmount", yuan(order.get("couponAmount")));
        result.put("discountAmount", yuan(couponAmount));
        result.put("payAmount", yuan(order.get("payAmount")));
        // 待支付页的补贴金额展示人才折扣与优惠券抵扣合计,即原价减去用户实付。
        result.put("subsidyAmount", yuan(Math.max(0, longNumber(order.get("orderAmount")) - longNumber(order.get("payAmount")))));
        result.put("subsidyAmount", yuan(Math.max(0, originalAmount - longNumber(order.get("payAmount")))));
        return result;
    }
    private List<Map<String, Object>> query(String sql, Object... args) { return jdbcTemplate.queryForList(sql, args).stream().map(DatabaseRows::normalize).toList(); }
    private BigDecimal decimal(Object value) {
        if (value == null) return BigDecimal.ZERO;
        if (value instanceof BigDecimal decimal) return decimal;
        try { return new BigDecimal(String.valueOf(value)); }
        catch (Exception ignored) { return BigDecimal.ZERO; }
    }
    private String activeCardLevel(Long talentId) {
        List<Map<String, Object>> cards = query("select card_level from talent_card where talent_id=? and deleted=0 and status=1 and card_type='PERSONAL' and (valid_end is null or valid_end >= current_date) order by id desc limit 1", talentId);
        if (cards.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "当前暂无有效人才卡");