如何在实际项目中正确使用Optional类?
约 5253 字大约 18 分钟
2025-09-23
在实际项目中,Optional
类的核心价值在于显式表达 "值可能缺失" 的语义,并通过函数式编程简化空值处理逻辑,减少 NullPointerException
。但如果使用不当,反而会让代码变得冗余晦涩。以下是实际项目中的正确使用指南和最佳实践:
一、核心原则:明确 Optional
的设计定位
Optional
是"方法返回值的包装器",用于明确告知调用者:"此方法可能返回空值,请妥善处理"。
禁止将其用作:
- 方法参数(会增加调用方复杂度)
- 类的成员变量(破坏序列化,且无实际意义)
- 集合元素(集合本身已有
isEmpty()
等方法判断空)
二、正确使用场景与示例
1. 作为方法返回值,替代直接返回 null
当方法可能返回 null
时(如数据库查询、远程调用),用 Optional
包装返回值,强制调用者处理空值情况。
反例(传统方式):
// 问题:调用者可能忘记判空,直接调用user.getName()导致NPE
public User findUserById(Long id) {
return userMapper.selectById(id); // 可能返回null
}
正例(使用 Optional):
// 明确告知调用者:结果可能为空
public Optional<User> findUserById(Long id) {
return Optional.ofNullable(userMapper.selectById(id));
}
// 调用方必须显式处理空值
Optional<User> userOpt = userService.findUserById(1L);
String userName = userOpt.map(User::getName) // 若存在则取名字
.orElse("未知用户"); // 若不存在则用默认值
2. 用链式调用替代嵌套的 if-else
Optional
的 map()
、flatMap()
、filter()
等方法支持链式调用,可将多层空值判断简化为流畅的函数式操作。
场景:获取 "用户的地址的城市名",需层层判空(用户可能为 null,地址可能为 null)。
反例(嵌套判空):
String city = "未知城市";
User user = userService.findUserById(1L);
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String tempCity = address.getCity();
if (tempCity != null) {
city = tempCity;
}
}
}
正例(Optional 链式调用):
String city = userService.findUserById(1L) // 返回Optional<User>
.map(User::getAddress) // 若用户存在,提取地址(Optional<Address>)
.map(Address::getCity) // 若地址存在,提取城市(Optional<String>)
.orElse("未知城市"); // 任意环节为空则用默认值
3. 用 ifPresent()
替代 "判空后执行操作"
当需要 "值存在时执行某个操作,否则不做处理" 时,用 ifPresent()
避免显式判空。
反例:
User user = userService.findUserById(1L);
if (user != null) {
log.info("用户存在:{}", user.getName());
sendWelcomeMsg(user); // 发送欢迎消息
}
正例:
userService.findUserById(1L)
.ifPresent(user -> { // 仅当用户存在时执行
log.info("用户存在:{}", user.getName());
sendWelcomeMsg(user);
});
4. 用 orElseThrow()
处理 "必须存在" 的场景
当业务要求 "值必须存在,否则抛异常" 时(如根据 ID 查询的记录必须存在),用 orElseThrow()
简化异常抛出逻辑。
反例:
User user = userService.findUserById(1L);
if (user == null) {
throw new BizException("用户不存在,ID:1");
}
正例:
User user = userService.findUserById(1L)
.orElseThrow(() -> new BizException("用户不存在,ID:1")); // 为空时直接抛异常
5. 结合集合 / Stream 处理批量空值
在处理集合时,用 Optional
过滤空元素,或转换为 Stream
进行批量处理。 示例:
List<String> userNames = userList.stream()
// 将每个元素包装为Optional,过滤空值
.map(Optional::ofNullable)
.filter(Optional::isPresent) // 保留非空元素
.map(Optional::get) // 提取值
.map(User::getName) // 提取用户名
.collect(Collectors.toList());
三、常见反模式(禁止使用)
1. 用 isPresent()
+ get()
替代 null
检查
这是最常见的错误用法,完全失去了 Optional
的意义,与传统 if (obj != null)
没有区别。 反例:
Optional<User> userOpt = userService.findUserById(1L);
if (userOpt.isPresent()) { // 等价于 if (user != null)
User user = userOpt.get(); // 等价于直接使用user
// ...
}
2. 用 Optional
包装已知非空的值
如果确定值一定非空,无需用 Optional
包装(徒增代码复杂度)。
反例:
// 已知"admin"非空,无需包装
Optional<String> adminOpt = Optional.of("admin");
String admin = adminOpt.get();
3. 过度链式调用导致可读性下降
虽然链式调用很优雅,但过长的链会导致逻辑晦涩,建议拆分复杂逻辑。
反例:
// 过长的链式调用,难以调试和维护
String result = service.findObj()
.map(Obj::getA)
.flatMap(A::getB)
.filter(B::isValid)
.map(B::getC)
.orElseThrow(...);
优化:
// 拆分步骤,提高可读性
Optional<Obj> objOpt = service.findObj();
Optional<A> aOpt = objOpt.map(Obj::getA);
Optional<B> bOpt = aOpt.flatMap(A::getB);
Optional<B> validBOpt = bOpt.filter(B::isValid);
String result = validBOpt.map(B::getC)
.orElseThrow(...);
4. 误用 orElse()
导致不必要的对象创建
orElse()
的参数无论值是否存在都会被执行,若默认值创建成本高(如数据库查询、复杂对象),应改用 orElseGet()
(延迟执行)。
反例:
// 即使user存在,getDefaultUser()也会被调用(浪费资源)
User user = userService.findUserById(1L)
.orElse(getDefaultUser()); // getDefaultUser()是耗时操作
正例:
// 仅当user不存在时,才调用getDefaultUser()
User user = userService.findUserById(1L)
.orElseGet(() -> getDefaultUser()); // 延迟执行
四、总结:Optional
的核心价值
- 显式化空值语义:通过方法返回值明确告知 "可能为空",强制调用者处理,减少 NPE。
- 简化代码逻辑:用函数式链式调用替代嵌套的
if-else
,让代码更流畅。 - 提高可读性:通过
map()
、orElse()
等方法,让空值处理逻辑一目了然。
记住:Optional
是 "工具" 而非 "银弹",合理使用能提升代码质量,过度或错误使用则会适得其反。
五、完整示例代码
/**
* 投标人多轮报价详情查询服务实现类
* 核心功能:查询指定标段的投标人报价详情,支持当前轮次与历史轮次查询,
* 处理已报价/未报价两种场景,并封装前端所需的VO对象
*/
@Service
public class BidderQuoteDetailServiceImpl implements BidderQuoteDetailService {
@Autowired
private MultiQuoteService multiQuoteService;
@Autowired
private MultipleQuoteBidderMapper multipleQuoteBidderMapper;
@Autowired
private RemotePurchasePackageService remotePurchasePackageService;
/**
* 获取投标人多轮报价详情的主入口方法
* 整体流程:参数校验→提取标段ID→查询最新报价→处理报价结果→返回封装后的VO
* 采用Optional链式调用实现空值安全处理,确保任何环节为空都不会抛出NPE
*
* @param query 报价查询参数对象,包含:
* - packageId:标段ID(必填,定位查询的标段)
* - bidderId:投标人ID(必填,定位查询的投标人)
* @return BidderMultipleQuoteVO 封装了报价详情的VO对象,包含:
* - 标段基础信息(编码、名称)
* - 报价记录(当前轮次或历史轮次)
* - 可报价状态(canQuote:是否仍可提交报价)
* - 报价表单详情(quoteForm:解析后的报价项列表)
* 注:即使无任何数据,也返回非null的VO对象
*/
@Override
public BidderMultipleQuoteVO getQuoteDetail(BidderMultipleQuoteQuery query) {
// 初始化返回结果VO,确保始终返回非null对象
BidderMultipleQuoteVO resultVO = new BidderMultipleQuoteVO();
// Optional链式调用处理空值安全流程:
// 1. 检查query是否为null → 2. 提取packageId → 3. 查询该标段最新报价 → 4. 处理报价结果
Optional.ofNullable(query)
.map(BidderMultipleQuoteQuery::getPackageId) // 提取标段ID(Function函数式接口)
.flatMap(this::getLastQuote) // 查询最新报价记录(返回Optional<MultipleQuote>)
.ifPresent(handleQuoteResult(resultVO, query)); // 处理报价结果(Consumer函数式接口)
return resultVO;
}
/**
* 构建报价结果处理器(核心业务流程封装)
* 封装"设置标段信息→查询投标人报价→分支处理已报价/未报价状态"的完整逻辑
* 返回Consumer函数便于在Optional链式调用中使用,实现逻辑解耦
*
* @param resultVO 待填充数据的结果VO对象
* @param query 查询参数对象,提供标段ID、投标人ID等条件
* @return Consumer<MultipleQuote> 接收最新报价记录的消费者函数:
* 当存在最新报价记录时,执行此函数完成数据封装
*/
private Consumer<MultipleQuote> handleQuoteResult(BidderMultipleQuoteVO resultVO, BidderMultipleQuoteQuery query) {
// Lambda表达式实现Consumer接口的accept方法
return lastQuote -> {
// 步骤1:设置标段基础信息(编码、名称等)到结果VO
setPackageInfo(resultVO, query.getPackageId(), lastQuote);
// 步骤2:查询该投标人在当前轮次的报价记录
Optional<MultipleQuoteBidder> quoteBidderOpt = findQuoteBidder(query, lastQuote);
// 步骤3:根据是否存在报价记录分支处理
if (quoteBidderOpt.isPresent()) {
// 已报价场景:应用已报价处理器
handleQuotedConsumer(resultVO, lastQuote).accept(quoteBidderOpt.get());
} else {
// 未报价场景:执行未报价处理器
handleUnquotedRunnable(resultVO, query, lastQuote).run();
}
};
}
/**
* 处理已报价状态的Consumer函数
* 封装已报价场景下的数据处理逻辑:属性复制、报价JSON解析、可报价状态判断
*
* @param resultVO 待填充的结果VO对象
* @param lastQuote 最新报价记录,提供报价截止时间等信息
* @return Consumer<MultipleQuoteBidder> 接收投标人报价记录的消费者函数:
* 执行时将报价信息填充到结果VO,并判断是否仍可修改报价
*/
private Consumer<MultipleQuoteBidder> handleQuotedConsumer(BidderMultipleQuoteVO resultVO, MultipleQuote lastQuote) {
// Lambda表达式实现Consumer接口
return quoteBidder -> {
// 复制报价记录的基础属性(id、totalPrice、quoteTime等)到结果VO
BeanUtil.copyProperties(quoteBidder, resultVO);
// 解析报价JSON字符串为表单VO列表(若存在)
Optional.ofNullable(quoteBidder.getQuoteJson())
.map(parseQuoteJson()) // 应用JSON解析函数(Function接口)
.ifPresent(resultVO::setQuoteForm); // 解析成功则设置到VO
// 判断是否仍可报价:当前时间在截止时间前 且 报价状态为"正常"
Optional.ofNullable(lastQuote.getQuoteEndTime())
.filter(endTime -> LocalDateTime.now().isBefore(endTime) // 时间条件:未到截止时间
&& BidderConstant.QUOTE_STATUS_NORMAL.equals(quoteBidder.getQuoteStatus())) // 状态条件:报价有效
.ifPresent(endTime -> resultVO.setCanQuote(true)); // 满足条件则设置可报价
};
}
/**
* 处理未报价状态的Runnable函数
* 封装未报价场景下的数据处理逻辑:查询历史报价参考、判断是否被邀请、设置可报价状态
*
* @param resultVO 待填充的结果VO对象
* @param query 查询参数对象
* @param lastQuote 最新报价记录,提供当前轮次ID和截止时间
* @return Runnable 无参数无返回值的函数:执行时处理未报价场景的数据封装
*/
private Runnable handleUnquotedRunnable(BidderMultipleQuoteVO resultVO,
BidderMultipleQuoteQuery query, MultipleQuote lastQuote) {
// Lambda表达式实现Runnable接口的run方法
return () -> {
// 子流程1:查询历史轮次报价作为参考(若存在)
findHistoryQuoteBidders(query, lastQuote)
.ifPresent(historyBidderQuote -> {
// 复制历史报价属性到结果VO
BeanUtil.copyProperties(historyBidderQuote, resultVO);
// 解析历史报价JSON为表单VO
Optional.ofNullable(historyBidderQuote.getQuoteJson())
.map(parseQuoteJson())
.ifPresent(resultVO::setQuoteForm);
});
// 子流程2:检查该投标人是否被邀请参与当前轮次报价
Optional<MultipleQuoteBidder> invitedBidder = queryQuoteBidder(
lastQuote.getId(),
query.getBidderId(),
query.getPackageId(),
null // 不限制状态,仅判断是否被邀请
);
// 子流程3:若已被邀请且在截止时间前,则设置可报价状态
if (invitedBidder.isPresent()) {
Optional.ofNullable(lastQuote.getQuoteEndTime())
.filter(endTime -> LocalDateTime.now().isBefore(endTime)) // 未到截止时间
.ifPresent(endTime -> resultVO.setCanQuote(true)); // 设置可报价
}
};
}
/**
* 解析报价JSON的Function函数
* 封装JSON字符串到报价表单VO列表的转换逻辑,便于在map操作中复用
*
* @return Function<String, List<MultipleQuoteFormVO>> 函数式接口实现:
* - 输入:报价信息JSON字符串(格式为数组)
* - 输出:解析后的MultipleQuoteFormVO对象列表(前端展示的报价项详情)
*/
private Function<String, List<MultipleQuoteFormVO>> parseQuoteJson() {
// Lambda表达式实现Function接口的apply方法
return json -> JSON.parseArray(json, MultipleQuoteFormVO.class);
}
/**
* 设置标段信息到结果VO
* 整合标段基础信息与最新报价记录,转换为MultipleQuoteVO并关联到结果对象
*
* @param resultVO 待填充的结果VO对象
* @param packageId 标段ID,用于查询标段基础信息
* @param lastQuote 最新报价记录,提供报价轮次ID、截止时间等信息
*/
private void setPackageInfo(BidderMultipleQuoteVO resultVO, Long packageId, MultipleQuote lastQuote) {
// 链式处理:查询标段信息→转换为VO→设置到结果
Optional.ofNullable(packageId)
.map(this::getPackageBaseInfo) // 调用远程服务查询标段基础信息
.ifPresent(packageVO -> {
// 自定义对象转换:将MultipleQuote转换为MultipleQuoteVO
MultipleQuoteVO multipleQuoteVO = ModelUtil.buildView(
lastQuote,
MultipleQuoteVO.class,
(source, target) -> { // Lambda表达式定义转换规则
target.setPackageCode(packageVO.getPackageCode()); // 填充标段编码
target.setPackageName(packageVO.getPackageName()); // 填充标段名称
target.setQuoteId(source.getId()); // 设置报价轮次ID(关联主记录)
}
);
// 将标段+报价轮次信息设置到结果VO
resultVO.setMultipleQuoteInfo(multipleQuoteVO);
});
}
/**
* 查找投标人报价(当前轮次)
* 封装当前轮次报价的查询逻辑,仅查询当前最新轮次的报价记录
*
* @param query 查询参数对象
* @param lastQuote 最新报价记录,提供当前轮次ID
* @return Optional<MultipleQuoteBidder> 包装了当前轮次报价记录的Optional:
* - 存在报价:返回包含记录的Optional
* - 无报价:返回空Optional
*/
private Optional<MultipleQuoteBidder> findQuoteBidder(BidderMultipleQuoteQuery query, MultipleQuote lastQuote) {
// 直接查询当前轮次报价(复用queryCurrentRoundQuote方法)
return queryCurrentRoundQuote(query, lastQuote);
}
/**
* 查询当前轮次报价
* 通过最新报价记录的ID,查询该投标人在当前轮次的有效报价(状态为"已报价")
*
* @param query 查询参数对象
* @param lastQuote 最新报价记录,提供当前轮次的主记录ID
* @return Optional<MultipleQuoteBidder> 包装了当前轮次有效报价记录的Optional
*/
private Optional<MultipleQuoteBidder> queryCurrentRoundQuote(BidderMultipleQuoteQuery query, MultipleQuote lastQuote) {
// 链式处理:从最新报价记录提取ID→查询对应投标人的有效报价
return Optional.ofNullable(lastQuote)
.map(MultipleQuote::getId) // 提取当前轮次报价主记录ID
.flatMap(quoteMainId -> queryQuoteBidder(
quoteMainId,
query.getBidderId(),
query.getPackageId(),
BidderConstant.QUOTE_STATUS_QUOTE // 仅查询"已报价"状态的记录
));
}
/**
* 查询历史轮次报价
* 从当前轮次向前迭代查询(当前轮次-1 → 1),包含死循环防护和业务规则校验
* 仅查询非首轮报价的历史记录(业务规则:首轮无历史可查)
*
* @param query 查询参数对象
* @param lastQuote 最新报价记录,提供当前轮次信息(确定查询范围)
* @return Optional<MultipleQuoteBidder> 包装了历史轮次报价记录的Optional:
* - 找到有效记录:返回包含记录的Optional
* - 未找到或不符合条件:返回空Optional
*/
private Optional<MultipleQuoteBidder> findHistoryQuoteBidders(BidderMultipleQuoteQuery query, MultipleQuote lastQuote) {
// 流程拆解:过滤首轮报价→迭代查询历史轮次→返回结果
return Optional.ofNullable(lastQuote)
.filter(this::isNotFirstRound) // 过滤首轮报价(业务规则:首轮不查历史)
.flatMap(quote -> {
int currentRound = quote.getRoundNum(); // 当前轮次(查询起点)
final int maxRounds = currentRound - 1; // 最大迭代次数(防止死循环)
int iterations = 0; // 迭代计数器(控制最大次数)
MultipleQuoteBidder quoteBidder = null; // 存储查询到的报价记录
// 循环查询条件:未找到报价 且 轮次>1 且 未超过最大迭代次数
while (Objects.isNull(quoteBidder) && currentRound > 1 && iterations < maxRounds) {
currentRound--; // 轮次递减(查询上一轮)
iterations++; // 计数器递增
// 查询当前轮次报价,若存在则赋值,否则保持null
quoteBidder = queryHistoryRoundQuote(query, currentRound)
.orElse(quoteBidder);
}
// 分支1:循环中找到报价记录,直接返回
if (Objects.nonNull(quoteBidder)) {
return Optional.of(quoteBidder);
}
// 分支2:轮次已减到1,查询第一轮报价
if (currentRound == 1) {
return queryFirstRoundQuote(query);
}
// 分支3:所有轮次查询无果,返回空Optional
return Optional.empty();
});
}
/**
* 查询指定历史轮次的报价
* 封装单轮历史报价的查询逻辑:先查该轮次主记录→再查对应投标人的有效报价
*
* @param query 查询参数对象
* @param round 要查询的历史轮次(正整数)
* @return Optional<MultipleQuoteBidder> 包装了该轮次有效报价记录的Optional
*/
private Optional<MultipleQuoteBidder> queryHistoryRoundQuote(BidderMultipleQuoteQuery query, int round) {
// 链式处理:查询该轮次主记录→提取主记录ID→查询对应投标人的有效报价
return Optional.ofNullable(multiQuoteService.queryLatestMultipleQuote(
query.getPackageId(), round, BidderConstant.QUOTE_ROUND_CANCEL))
.map(MultipleQuote::getId) // 提取该轮次报价主记录ID
.flatMap(quoteMainId -> queryQuoteBidder(
quoteMainId,
query.getBidderId(),
query.getPackageId(),
BidderConstant.QUOTE_STATUS_QUOTE // 仅查询"已报价"状态
));
}
/**
* 查询第一轮报价
* 专门处理第一轮报价的查询逻辑(作为历史查询的最后尝试)
* 注:首轮报价无需手动结束,因此"已报价"状态即有效
*
* @param query 查询参数对象
* @return Optional<MultipleQuoteBidder> 包装了第一轮有效报价记录的Optional
*/
private Optional<MultipleQuoteBidder> queryFirstRoundQuote(BidderMultipleQuoteQuery query) {
// 固定查询第1轮报价(复用单轮查询逻辑)
return Optional.ofNullable(multiQuoteService.queryLatestMultipleQuote(
query.getPackageId(), 1, BidderConstant.QUOTE_ROUND_CANCEL))
.map(MultipleQuote::getId)
.flatMap(quoteMainId -> queryQuoteBidder(
quoteMainId,
query.getBidderId(),
query.getPackageId(),
BidderConstant.QUOTE_STATUS_QUOTE
));
}
/**
* 查找历史报价记录(用于未报价时的参考)
* 从当前轮次-1开始向前查询,直到找到包含有效报价JSON的记录(非空)
* 用于未报价场景下展示历史报价作为参考
*
* @param query 查询参数对象
* @param lastQuote 最新报价记录,提供当前轮次信息
* @return Optional<MultipleQuoteBidder> 包装了有效历史报价记录的Optional:
* - 找到有有效JSON的记录:返回包含记录的Optional
* - 未找到或无有效JSON:返回空Optional
*/
private Optional<MultipleQuoteBidder> findHistoryQuoteBidder(BidderMultipleQuoteQuery query, MultipleQuote lastQuote) {
// 流程拆解:计算起始轮次→迭代查询→过滤有效记录→返回结果
return Optional.ofNullable(lastQuote)
.map(quote -> quote.getRoundNum() - 1) // 起始轮次:当前轮次-1
.flatMap(roundNum -> {
MultipleQuoteBidder quoteBidderCur = null; // 存储查询到的记录
final int maxRounds = roundNum; // 最大迭代次数(防止死循环)
int iterations = 0; // 迭代计数器
// 循环条件:轮次≥1 且 未超过最大迭代次数
while (roundNum >= 1 && iterations < maxRounds) {
iterations++; // 计数器递增
// 查询当前轮次报价,并过滤出包含有效JSON的记录
quoteBidderCur = queryHistoryRoundQuote(query, roundNum)
.filter(bidder -> Objects.nonNull(bidder.getQuoteJson())) // 有效JSON校验(非空)
.orElse(quoteBidderCur);
// 找到有效记录则退出循环
if (ObjectUtil.isNotNull(quoteBidderCur)) {
break;
}
roundNum--; // 轮次递减
}
// 仅当轮次有效(≥1)时返回记录,否则返回空
return Optional.ofNullable(roundNum >= 1 ? quoteBidderCur : null);
});
}
/**
* 通用报价查询方法
* 根据报价主记录ID、投标人ID、标段ID查询唯一的报价记录,支持按状态过滤
* 封装重复的查询条件,减少代码冗余
*
* @param quoteMainId 报价主记录ID(关联到具体轮次)
* @param bidderId 投标人ID(查询的目标投标人)
* @param packageId 标段ID(查询的目标标段)
* @param quoteStatus 报价状态(可选,为null时不限制状态)
* @return Optional<MultipleQuoteBidder> 包装了查询结果的Optional:
* - 存在匹配记录:返回包含记录的Optional
* - 无匹配记录:返回空Optional
*/
private Optional<MultipleQuoteBidder> queryQuoteBidder(Long quoteMainId, Long bidderId, Long packageId, String quoteStatus) {
// 构建查询条件:必选条件(主记录ID+投标人ID+标段ID)+ 可选条件(状态)
return Optional.ofNullable(multipleQuoteBidderMapper.selectOne(
Wrappers.<MultipleQuoteBidder>lambdaQuery()
.eq(MultipleQuoteBidder::getQuoteMainId, quoteMainId)
.eq(MultipleQuoteBidder::getBidderId, bidderId)
.eq(MultipleQuoteBidder::getPackageId, packageId)
.eq(StrUtil.isNotBlank(quoteStatus), MultipleQuoteBidder::getQuoteStatus, quoteStatus) // 状态条件(可选)
));
}
/**
* 获取最新报价记录
* 查询指定标段的最新一轮报价主记录(用于确定当前轮次信息)
*
* @param packageId 标段ID
* @return Optional<MultipleQuote> 包装了最新报价记录的Optional:
* - 存在报价记录:返回包含记录的Optional
* - 无报价记录:返回空Optional
*/
private Optional<MultipleQuote> getLastQuote(Long packageId) {
return Optional.ofNullable(multiQuoteService.getLastQuote(packageId));
}
/**
* 获取标段基础信息
* 远程查询标段的基础信息(编码、名称等),用于前端展示
*
* @param packageId 标段ID
* @return PurchasePackageVO 标段基础信息VO,可能为null(查询失败时)
*/
private PurchasePackageVO getPackageBaseInfo(Long packageId) {
// 调用远程服务查询,并解析返回结果(封装远程调用的通用逻辑)
return RemoteParseUtil.getData(remotePurchasePackageService.getPackageBaseInfo(packageId));
}
/**
* 检查是否非首轮报价(业务规则校验)
* 用于过滤首轮报价场景(业务规定:首轮报价无历史记录可查询)
*
* @param quote 报价记录,包含轮次信息
* @return boolean 校验结果:
* - true:非首轮报价(可以查询历史记录)
* - false:首轮报价(禁止查询历史记录)
*/
private boolean isNotFirstRound(MultipleQuote quote) {
return !BidderConstant.FIRST_QUOTE_ROUND.equals(quote.getRoundNum());
}
// ------------------------------ 使用示例代码 ------------------------------
/**
* 投标人多轮报价详情查询的使用示例
* 包含三种典型场景:已报价、未报价但有历史记录、未报价且无历史记录
*/
public static class UsageExample {
@Autowired
private BidderQuoteDetailService quoteDetailService;
/**
* 场景1:投标人已提交当前轮次报价
* 预期结果:返回当前报价信息,包含可修改状态(若在截止时间内)
*/
public void testQuotedScenario() {
// 1. 构建查询参数(标段ID=1001,投标人ID=2001)
BidderMultipleQuoteQuery query = new BidderMultipleQuoteQuery();
query.setPackageId(1001L);
query.setBidderId(2001L);
// 2. 调用查询方法
BidderMultipleQuoteVO result = quoteDetailService.getQuoteDetail(query);
// 3. 处理结果
System.out.println("标段名称:" + result.getMultipleQuoteInfo().getPackageName());
System.out.println("当前轮次:" + result.getMultipleQuoteInfo().getRoundNum());
System.out.println("报价总金额:" + result.getTotalPrice());
System.out.println("是否可修改报价:" + result.isCanQuote()); // 若在截止时间内则为true
System.out.println("报价项数量:" + result.getQuoteForm().size());
}
/**
* 场景2:投标人未提交当前轮次报价,但有历史轮次报价
* 预期结果:返回历史报价信息作为参考,包含可报价状态(若已被邀请且在截止时间内)
*/
public void testUnquotedWithHistoryScenario() {
// 1. 构建查询参数(标段ID=1001,投标人ID=2002)
BidderMultipleQuoteQuery query = new BidderMultipleQuoteQuery();
query.setPackageId(1001L);
query.setBidderId(2002L);
// 2. 调用查询方法
BidderMultipleQuoteVO result = quoteDetailService.getQuoteDetail(query);
// 3. 处理结果
System.out.println("当前轮次是否已报价:否(展示历史报价)");
System.out.println("历史轮次:" + result.getRoundNum()); // 显示最近一次历史轮次
System.out.println("历史报价金额:" + result.getTotalPrice());
System.out.println("是否可提交报价:" + result.isCanQuote()); // 若被邀请且在截止时间内则为true
}
/**
* 场景3:投标人未提交当前轮次报价,且无历史报价记录
* 预期结果:仅返回标段和轮次信息,无报价数据,包含可报价状态
*/
public void testUnquotedWithoutHistoryScenario() {
// 1. 构建查询参数(标段ID=1001,投标人ID=2003)
BidderMultipleQuoteQuery query = new BidderMultipleQuoteQuery();
query.setPackageId(1001L);
query.setBidderId(2003L);
// 2. 调用查询方法
BidderMultipleQuoteVO result = quoteDetailService.getQuoteDetail(query);
// 3. 处理结果
System.out.println("标段信息:" + result.getMultipleQuoteInfo().getPackageName());
System.out.println("当前轮次:" + result.getMultipleQuoteInfo().getRoundNum());
System.out.println("是否有历史报价:" + (result.getQuoteForm() == null ? "否" : "是"));
System.out.println("是否可提交报价:" + result.isCanQuote()); // 取决于是否被邀请和时间
}
}
}