主题
商户详情价格修复与验证
验证日期:2026-10-11。对应当前工作区修复,基于 70a3495b。
AdminWeb 商户详情中的六项已确认问题已修复,操作说明同步更新至 价格调整操作说明。
| 问题 | 修复后的行为 | 验证结果 |
|---|---|---|
| 金额解析错误 | 按整数分精确转换并保留负号;底价拒绝负数、零、科学计数法、非数字字符和超过两位小数;输入后立即同步,保存不依赖失焦 | 页面输入 -0.50 不再变成正数;321.23 直接保存为 32123 分 |
| 车辆日期底价并发覆盖 | 先锁车辆,再锁日期行并核对当前底价;同一个旧价格不能连续覆盖两次 | 真实 MySQL 两个等待锁的请求只有一个成功,仅产生一条有效改价历史 |
| 日期售价导致券预约失败 | 日期售价必须同时覆盖日期结算底价和预约商户加价;调整默认售价时也检查已有日期加价 | 默认售价 360 元、商户日期加价 20 元时,拒绝 370 元,接受 380 元;预约用户与商户加价均为 20 元 |
| 套餐供给底价被零售价限制 | 套餐供给底价可独立调整,不展示独立零售价和不支持的日期改价入口 | 自营套餐底价从 300 元提高至 310 元成功,日期接口明确拒绝不支持的操作 |
| 房型缺少默认售价入口 | 售价弹窗支持默认售价和日期售价;默认售价接口校验旧值、记录历史并保留已有日期价格 | 默认售价调整后,已有日期售价不变;提高默认售价后可继续调整默认底价 |
| 同价日期底价被默认底价覆盖 | 用持久化标记区分默认继承和独立日期底价;单独设置的同价日期仍独立 | 独立日期底价保持 300 元,继承日期随默认底价更新;自动月度发布保留已配置日期价格 |
金额单位仍为分。平台运营商品统一底价仍同步团购、拼团和套餐供给;团购、拼团售价分别设置。没有商户日期加价的日期仍可使用合法折扣,批量失败仍整体回滚。
验证范围
| 检查 | 结果 |
|---|---|
| 后端价格、库存、日历、事务回归 | 138 项通过,0 失败、0 错误;最终关联调整的 43 项再次通过 |
| 迁移资源契约 | 1 项通过 |
| 前端金额与价格契约 | 11 项通过 |
| 运行中的 API 与真实 MySQL | 18 项通过,包括车辆并发冲突、旧值冲突和批量回滚 |
| 运行中的 AdminWeb 浏览器操作 | 10 项通过,0 未捕获脚本异常;另通过组件浏览器回归与手机布局检查 |
| 实际业务类与数据库报价核对 | 4 项通过:日期订单报价、券预约加价、合法折扣、自动月度房态发布 |
| 构建 | API Maven 构建、AdminWeb 生产构建通过 |
| Flyway | 新增迁移在验证库执行成功,58 条迁移校验通过 |
后端 JUnit 5 回归使用 Maven Surefire 3.5.3 显式执行,未把默认旧版 Surefire 的跳过结果计为通过。
WSL 中运行 API 和 AdminWeb,MySQL、Redis 通过 SSH 使用开发服务器。所有写入均在独立克隆验证库及 Redis DB 15,原开发业务库和生产服务未修改。预约验证调用实际计价类并读取真实 MySQL 日历,未发起真实支付。
本地验证结果保存在 /home/ubuntu/.local/share/xiedao/wsl-dev/:price-fix-api-results.json、price-fix-browser-results.json、price-fix-quote-results.json,截图位于其 artifacts/ 目录。AdminWeb 地址为 http://localhost:8081/merchant-center/merchant,API 地址为 http://localhost:8082。
数据库迁移
发布修复版 API 前执行 V20261011_001__room_calendar_base_price_override.sql,给 xd_room_calendar 增加 base_price_override:
0:自动生成、跟随房型默认底价的日期。1:独立设置的日期底价。
历史日历缺少设置意图,迁移统一按独立日期处理,保留价格;不会仅凭金额相同推断为默认继承。新生成的默认房态明确写入 0,日期底价编辑或月度审核发布写入 1。仅修改日期售价或库存时保留底价继承状态。
自动月度发布继承已有日期价格并保留继承标记,补齐缺失日期;月度申报记录与保留的日期价格保持一致。迁移在本次验证库已执行;开发、正式环境发布仍按仓库既有 Flyway 流程执行。
本轮价格逻辑审查项修复(2026-10-11)
对应 商户详情价格逻辑审查,本轮代码基线为 30ca0102。已完成以下修复:
| 审查项 | 修复结果 |
|---|---|
| 历史日期和范围上限 | 日期选择器与表格勾选禁止历史日期;加载前和提交前校验上海业务日期及 366 天上限,非法范围不会请求后端 |
| 底价上限 | 新增带查看权限的单对象价格详情 GET;默认底价弹窗展示最新底价、各活动售价、最高可设底价及限制来源。上限包含未来日期结算加价、各非套餐活动,以及跟随默认底价的房态日期。日期底价也返回并校验逐日上限 |
| 并发冲突和失败恢复 | 默认底价、日期底价、售价弹窗提供内联错误和“刷新价格”;刷新保留金额与原因,日期刷新保留仍可编辑的勾选,更新旧值后再保存。打开默认底价弹窗会重新获取当前价格 |
| 空金额与格式 | 金额展示统一使用 displayMoney,空值显示 -,零显示 ¥0.00,日历保留两位小数。原审查关于数字字符串不安全的说法不成立 |
| 时区 | 三个编辑入口复用上海业务日期,售价弹窗每次打开重新计算今天;底价和售价后端历史日期校验也明确使用 Asia/Shanghai |
| 无变化提交 | 前后端均跳过真正无变化的日期,不重复记录审计或通知;缺少日期配置、房型从默认继承转为独立日期底价仍允许同价保存 |
| 加载失败 | 价格列表捕获请求拒绝并提供重试,保存后重载保留原表格;失败时明确提示列表可能不是最新数据 |
| 空底价补设 | 提供“设置底价”,后端支持空旧值和零旧值的默认底价初始化。四类对象保留锁及 SQL 条件更新,空旧值不能覆盖已配置价格。审计与通知保留原价为空,商户通知显示“未配置” |
本轮确认原审查的“保存失败后输入全部丢失”不成立:原代码本来就保留输入,但缺少刷新旧值和内联恢复入口。后端整批回滚继续保留,历史日期不能导致部分改价成功。
| 验证 | 结果 |
|---|---|
| 前端金额、日期范围、上限、同价配置与既有价格/库存契约 | 19 项通过 |
| 后端价格、库存、历史、通知及 SQL/事务定向回归 | 95 项通过,0 失败、0 错误、0 跳过;显式使用 Surefire 3.5.3 |
| 商户详情浏览器回归 | 9 个场景通过:上限拦截、默认底价冲突保留输入、刷新旧值、重载失败保留表格、上海日期及历史勾选、同价配置与无效行过滤、日期冲突恢复、详情加载重试、空底价初始化;0 未捕获页面异常 |
| 售价浏览器回归 | 酒店日历、活动切换、校验与载荷、手机布局、默认价批量编辑均通过;并发冲突刷新、刷新失败后重试均保留输入 |
| 构建与脚本检查 | API、AdminWeb、MerchantWeb 生产构建通过;MerchantApp 通知脚本语法检查通过;Git 差异空白检查通过 |
本轮浏览器通过本地 Vite 加载真实 Vue 页面/组件,接口使用模拟响应;SQL 与事务测试使用 H2 的 MySQL 模式,未连接业务数据库或发送真实商户通知。新测试包括 AdminWeb/tests/merchant-settlement-price.test.mjs、AdminWeb/tests/merchant-settlement-price.browser.mjs 和 MerchantSettlementPriceInitializationMapperTest,并扩展现有房型事务与售价浏览器回归。
本轮不新增数据库迁移;空原价审计使用既有 V20260920_001__merchant_inventory_management.sql 中的可空金额列。发布时 API 与 AdminWeb 需一起更新,以使用新的价格详情和日期约束字段。最新操作流程已同步至 价格调整操作说明。
本轮价格影响面与可操作性修复(2026-10-16)
本轮先做了一次以商品数据结构为准的复核:把后端 MerchantSettlementPriceService、MerchantSalePriceService、PriceCalendarInvariant 的规则逐条转写为对照矩阵,与前端校验函数比对。价格计算本身未发现偏差(整分为单位、底价≤售价允许相等、日期补款下限、房型底价联动保售价、酒店改默认售价保日期总价、366 天边界),因此本轮只修非数值面的问题。
| 编号 | 严重度 | 问题 | 修复结果 |
|---|---|---|---|
| F1 | 高 | 房型默认底价保存时会同步改写全部「跟随默认底价」的未来房态(updateFutureDefaultPriceKeepingSale),而变更记录只有一条 price_date = null 的默认价明细,商户通知同样只报 1 项 | 保存前在弹窗展示「同步改写 N 个日期」并列出日期(超过 20 个折叠为「等 N 个」);改写日期写入 xd_merchant_settlement_price_change.affected_dates,变更明细弹窗与商户通知卡片都带上完整日期清单 |
| F2 | 高 | 日期底价弹窗对「该日售价已低于结算底价」的存量行仍开放勾选,但下限(默认底价)高于上限(该日售价),任何输入都被拒且两条提示互相矛盾 | 新增 priceRangeConflict,该类行直接不可勾选并在表格「提示」列给出唯一可执行路径;保存校验优先报同一条消息 |
| F3 | 中高 | 租车行的「默认平台售价」来自平台车型库 xd_car_model.daily_price_fen,被同车型所有商户共用,且本页无修改入口 | 列名与弹窗标注为「车型日租售价(平台统一)」并说明不能在商户详情页修改 |
| F4 | 中 | maximumPriceReason 只返回所有活动 × 所有未来日期里最紧的一条原因,运营无法预判该改哪个活动 | 新增 maximumPriceLimits,按金额从低到高返回至多 10 条约束;弹窗改为列出全部约束(仅 1 条时保持原样) |
| F5 | 低中 | 售价弹窗用 Number(row.basePriceFen),接口返回 null 时会变成 0,提交后被回成「售价或底价已变化」 | 提交前显式判空并提示「结算底价或平台售价未配置,请刷新价格后重试」 |
| F6 | 低 | updateCalendarRow 在活动未配置结算底价时对 null 调用 .add() 直接 NPE(前端已挡住,直调 API 可达) | 补「活动未配置结算底价,不能修改日期底价」显式校验 |
数据库迁移
新增 V20261016_001__merchant_settlement_price_affected_dates.sql,给 xd_merchant_settlement_price_change 增加可空 affected_dates(逗号分隔的 yyyy-MM-dd)。选 TEXT 而非定长,因为跟随房型默认底价的历史日期可能远多于 366 天。
审计语义
改一次房型默认底价仍然只写一条变更明细(ROOM_TYPE / price_date = null),商户也只收到一条通知;被连带改写的日期作为该条记录的 affectedDates 清单随行。这与「批量修改日期底价」按日期逐条记录的既有语义不同,不要把两者混为一谈。初始化底价(expectedOldPriceFen 为空)不会改写任何日期,该字段为空。
本轮验证
| 验证 | 结果 |
|---|---|
后端 ruoyi-system 全量测试 | 2169 项通过,0 失败、0 错误、0 跳过 |
| 新增后端用例 | 房型改默认底价记录同步改写日期、初始化无改写日期、多约束清单、套餐无上限、活动无底价不再 NPE、affected_dates 落库读回、通知描述与卡片 |
前端 node --test tests/*.test.mjs | 44 项通过 |
| 新增前端用例 | 区间冲突识别与不可保存、勾选拦截原因、改价弹窗影响面与约束清单契约、售价弹窗空值拦截契约 |
merchant-settlement-price.browser.mjs | 已补「绑定约束清单 / 改写日期预览 / 区间倒挂日期不可选」三个场景;本机未安装 Playwright 与浏览器二进制,本轮未实际执行,需在具备 Playwright 的环境补跑 |
| AdminWeb 生产构建 | 通过 |
发布时 API 与 AdminWeb 需一起更新:AdminWeb 使用了新增的 maximumPriceLimits、affectedCalendarDateCount、affectedCalendarDates 字段,先发 API 再发前端。数据库迁移按仓库既有 Flyway 流程执行。