hl-api-changelog/changelogs/2026-04/30_feat_product_admin-min-price-api.md

5.5 KiB

新增管理端接口:产品最低价(用于编辑页"行程报价"预览)

类型: 后端 FEAT(新增接口) + 前端切换通知 关联: 工单 #1556 / PR #1558 / Release PR #1559 日期: 2026-04-30 前端处理者: mmg 影响范围: 管理后台产品编辑页右侧"预览面板 → 行程报价"区域


现象(BUG)

管理后台产品编辑页 (web.test.1814.love:9443/product/edit?id=...) 右侧预览面板的"行程报价"区域,两个档位(舒适/高档)都显示 ¥-- 起/成人,即使 Step 4 定价管理已实际配置价格(用户截图: 平日价 ¥45,455 已落 product_price_calendar)。

根因

不是字段 BUG,详情接口 /admin/product/item/{id} 已通过 PriceCalendarAggregator 回填 startPrice/tierPrices, 但 3 个边界场景导致预览 ¥--:

  1. 全过期 calendarkeepEmptyTiers=false 把无价档全过滤掉
  2. product.tiers 配多档但 calendar 只配了 tier 1 → tier 2 在详情接口被剔除前端拿不到
  3. 详情接口 17 次 DB 读太重, Step 4 改完价后预览不刷新

解法 — 新增专用轻量起价接口

GET /admin/product/item/{productId}/min-price
Authorization: Bearer {adminToken}

Response (ProductMinPriceRespVO)

{
  "code": 0,
  "message": "success",
  "data": {
    "productId": "2049468625294708737",
    "productType": "CUSTOM",
    "priceSource": "PRICE_CALENDAR",   // PRICE_CALENDAR=价格日历, GROUP_BATCH=班期表
    "globalMinAdultPrice": 45455.00,    // 全档汇总最低成人价(无价时 null)
    "currency": "CNY",
    "futurePriceFromDate": "2026-05-01",
    "futurePriceToDate":   "2026-05-31",
    "hasMultiTier": true,                // tiers.size > 1
    "tiers": [
      {
        "tierSeq": 1,
        "tierName": "舒适",
        "tierDescription": "4钻酒店",
        "minAdultPrice": 45455.00,        // null 表示该档无未来可售价
        "priceFromDate": "2026-05-01",
        "priceToDate":   "2026-05-31"
      },
      {
        "tierSeq": 2,
        "tierName": "高档",
        "tierDescription": "5星酒店",
        "minAdultPrice": null,             // 未配置 → null,前端按 null 显 ¥--
        "priceFromDate": null,
        "priceToDate":   null
      }
    ]
  }
}

字段说明

  • productType 字典 product_type: CORE=核心 / GROUP=小蒙马 / CUSTOM=私人定制
  • priceSource 字典(代码内):
    • PRICE_CALENDAR — CORE/CUSTOM 走 product_price_calendar,按 tierSeq 分组取 MIN(adultSellPrice)
    • GROUP_BATCH — GROUP 走 group_tour_batch.adult_price MIN
  • 仅查未来可售(date >= todayadultSellPrice > 0)
  • 保留所有档位: 即使该档无价,tiers[] 里仍有该档的元素,minAdultPrice=null — 前端按 null 显 ¥-- 与列表语义一致
  • globalMinAdultPrice 是所有 tier 中非 null 价的全局 min,前端"整体起价"用
  • DB 读仅 2 次(产品主表 + 价格源),详情接口 17 次

错误码

  • code=0 — 成功
  • code=404 — 产品不存在或当前 admin 无 DataScope 权限(行级权限拦截)

前端要做的改动 ⚠️

替换数据源

hl-ui 中产品编辑页右侧 MobilePreview.vue / PreviewPricing.vue 等组件,把"行程报价"区的取价逻辑由"复用详情接口的 startPrice/tierPrices"改为单独调用 GET /admin/product/item/{id}/min-price

触发时机

  • 进入编辑页:产品基础信息加载完之后调一次
  • Step 4 定价管理保存成功后:主动 refetch /min-price(详情接口缓存了 17 次 DB 聚合,不会立刻反映改价;新接口 2 次 DB 读,可频繁刷)
  • 切换 Step 时无需 refetch(价格不会因切 Step 变化)

渲染规则

  • data.tiers 非空 → 按 tierSeq 排序逐档渲染
    • minAdultPrice != null → 显示金额 + "起/成人"
    • minAdultPrice == null → 显示 ¥-- 起/成人(用户配置了该档位但未配价)
  • data.globalMinAdultPrice == null && data.tiers 全 null → 整个面板都显 ¥--(产品完全没配价)
  • 不需要再走老的 tierPrices/startPrice 字段链路降级 — 新接口语义完整覆盖

测试场景(测试服已 round-trip 通过)

场景 接口返回 前端预期
双档全有价 tiers[0].min=7960, tiers[1].min=7960, hasMultiTier=true 两档都显 ¥7960 起
单档已配价 tiers[0].min=1440, hasMultiTier=false 一档显 ¥1440 起
双档配了 tier1 没配 tier2 tiers[0].min=有价, tiers[1].min=null 舒适显价 + 高档显 ¥--
全过期/未配价 globalMinAdultPrice=null, tiers 全 null 全档 ¥--
GROUP 小蒙马 priceSource=GROUP_BATCH, 价格从班期表取 MIN 同 CORE 渲染

接口参考产品(测试服)

  • 2047268852147875842 — PUBLISHED CORE 双档舒适+品质,全 7960
  • 2046104743003971585 — DRAFT CORE 单档轻奢,1440

部署状态

  • PR #1558 合并到 dev (sha 30904060)
  • Deploy Panel task 1b109e5c success, 双实例 hl-product-service-v2:8083+8183 滚动重启完成
  • 测试服经网关 9443 round-trip 3 个产品全通过
  • Release PR #1559 dev → main 待用户网页合,合后通知正式服务器管理员部署正式

向后兼容

  • /admin/product/item/{id} 详情接口字段完全不变(仍返 tierPrices/startPrice/priceCalendars)
  • 前端老的 MobilePreview.vue / PreviewPricing.vue 三级 fallback 链路不会因后端报错(只是仍显 ¥--)
  • 切换可分阶段:先把"行程报价"区切到新接口,其他依赖 startPrice/tierPrices 的地方按需后切