1 问题背景
1.1 什么是验机报告?
众所周知,转转是一个官方验的二手交易平台,对于用户在其平台上交易的商品,均会由专业的质检工程师进行验证。这项验证将会产生一份详尽的报告,这就是所谓的验机报告。
1.2 为什么要做验机报告的统一?
对于C2B线上回收来说,质检部门提供的验机报告具有原始性,其中可能包含一些专业术语。如果直接展示给用户,用户可能会感到困惑。因此我们都会对验机报告进行一层包装再呈现给用户。
早期C2B线上回收给用户展示的验机报告是瑕疵项报告,这些报告在手机品类和数码品类的展示方式也存在差异。此外,客服人员所能查看的用户验机报告与用户自己所看到的报告也存在差异。再后来随着差异项报告的加入,就更加混乱了。同一个商品,C2B存在多份验机报告,验机报告的链接也不一样。为了解决这个问题,我们推动了验机报告的统一。
2 C2B用户验机报告的演进
2.1 瑕疵项报告
瑕疵项报告将质检结果中的优点和问题逐一列举,以供用户查阅。判断质检项的优劣是根据我们自身的阿波罗配置进行的。阿波罗的配置工作由运营人员负责维护,如果质检侧出现了新的判定属性,就需要同步更新阿波罗的配置。瑕疵项验机报告的效果如下图所示:
图片
2.2 差异项报告
差异项报告通过比较用户选择的内容与验机报告中的实际内容,可以体现出偏好、偏差,或者一致性。将用户选择的内容与实际检测结果并排展示,可以使用户一目了然,从而提升用户体验。如何将实际质检项与用户选择的项映射起来,判定结果好坏与否,这个数据我们是维护在数据库中,同时也做了对应的后台交给运营去配置。差异项验机报告的效果如下图所示:
图片
2.3 验机报告统一
所谓验机报告的统一,意味着前后端都进行了一致的优化。前端页面上的验机报告入口链接已经得到了一致的处理,后端验机报告接口也得到了统一的收口管理。通过采用不同的策略,不同的商品能够匹配到相应的报告类型。经过多次实验验证,我们确定了优先级顺序:差异项报告 > 瑕疵项报告 > 兜底报告。
图片
图片
3 差异项报告系统设计
差异项验机报告对比了用户预估报告和实际验机报告,很多业务都会有用户和实际质检两份报告,对比这两份报告是一个通用的能力,因此我们对差异项报告能力做了下沉。
3.1 对比映射模板
对比映射模板本质上是实际质检项与用户预估项之间的映射关系。由于实际质检的项数可能远多于用户选择的项数,并且实际质检中的许多项需要映射到用户选择的某一项,因此我们需要配置这些项之间的映射规则。
- 项比较多:使用Excel表格来进行数据管理,并将其存储到数据库中。
- 品类:以品类为维度进行区分,针对不同品类配置不同模板。
- 特殊情况:同品类下可针对特殊机型配置单独的模板。
3.2 业务配置
在映射模板配置完成后,进一步进行业务线的配置变得必要。由于预质检和实际质检是两个独立的业务线,必须明确指定业务线,以确保映射关系能够正确对应。此外,加入业务线的配置也为未来其他业务的接入提供了方便。
图片
3.3 映射解析
在差异项配置完成后,当符合条件的商品出现时,验机报告会根据我们预先配置的模板进行解析。
/**
* 对比报告 A' 和 B
* 1.设备属性信息对比
* 2.单选项对比
* 3.多选项对比
* 4.根据选项类别分组,单选项先按照A价格影响因子倒序+多选项的排序结果
* 5.组合标签标题
*/
public void compareReport(QcInfoAfterMappingBo qcInfoAfterMappingSrcBo, QcInfoAfterMappingBo qcInfoTargetBo,QcDiffTemplateBo qcDiffTemplateBo, QcDiffDetailDTO qcDiffDetailDTO) {
//B报告到A报告的映射
Map<String, List<QcDiffItemsMapping>> targetToSrcMap = qcDiffTemplateBo.getQcDiffItemsMappings().stream().collect(Collectors.groupingBy(QcDiffItemsMapping::getValueIdTarget));
//A报告 valueId -> qcItemMap
Map<String, QcInfoAfterMappingBo.ItemInfo> qcSrcValueIdMap = qcInfoAfterMappingSrcBo.getNoMappingItemInfoList().stream().collect(Collectors.toMap(QcInfoAfterMappingBo.ItemInfo::getValueId,Function.identity()));
//B报告基本信息映射 valueIdTarget -> targetConfigMap
Map<String, QcDiffTargetConf> targetConfMap = qcDiffTemplateBo.getDiffTargetConfMap();
//以A'为模板来对比
Map<String, QcInfoAfterMappingBo.ItemInfo> singleSelectMap = qcInfoAfterMappingSrcBo.getMappingSingleSelectMap();
List<QcInfoAfterMappingBo.ItemInfo> noMappingTargetItemInfoList = qcInfoTargetBo.getNoMappingItemInfoList();
if (CollectionUtils.isEmpty(noMappingTargetItemInfoList)) {
return;
}
Map<String, List<QcInfoAfterMappingBo.ItemInfo>> targetItemMap = noMappingTargetItemInfoList.stream().collect(Collectors.groupingBy(QcInfoAfterMappingBo.ItemInfo::getItemId));
QcDiffCompareBo compareBo = QcDiffCompareBo.builder().singleSelectItemsDetailList(Lists.newArrayList()).multiSelectItemsDetailList(Lists.newArrayList()).optionTypeDiffLevelNumMap(new HashMap<>()).build();
//设备属性对比
deviceInfoCompare(qcInfoAfterMappingSrcBo, qcInfoTargetBo, qcDiffDetailDTO);
//单选结果组合
singleSelectCompare(singleSelectMap, targetConfMap, targetItemMap, targetToSrcMap, qcSrcValueIdMap, compareBo);
//多选结果组合
Map<String, List<QcInfoAfterMappingBo.ItemInfo>> multiSelectMap = qcInfoAfterMappingSrcBo.getMappingMultiSelectMap();
multiSelectCompare(multiSelectMap,targetItemMap, targetToSrcMap, qcSrcValueIdMap, targetConfMap, compareBo);
//根据组合结果中报告类型分组
Map<Integer, List<QcDiffItemsInfo.ItemsDetail>> singleResultListMap = compareBo.getSingleSelectItemsDetailList().stream().collect(Collectors.groupingBy(QcDiffItemsInfo.ItemsDetail::getOptionType));
Map<Integer, List<QcDiffItemsInfo.ItemsDetail>> multiResultListMap = compareBo.getMultiSelectItemsDetailList().stream().collect(Collectors.groupingBy(QcDiffItemsInfo.ItemsDetail::getOptionType));
List<QcDiffItemsInfo> qcDiffItemsInfos = new ArrayList<>();
for (OptionTypeEnum optionTypeEnum : OptionTypeEnum.values()) {
QcDiffItemsInfo qcDiffItemsInfo = new QcDiffItemsInfo();
qcDiffItemsInfo.setName(optionTypeEnum.getDesc());
List<QcDiffItemsInfo.ItemsDetail> itemsDetailList = new ArrayList<>();
if (singleResultListMap.containsKey(optionTypeEnum.getCode())) {
//组合标签内容
itemsDetailList = singleResultListMap.get(optionTypeEnum.getCode()).stream().sorted(Comparator.comparing(QcDiffItemsInfo.ItemsDetail::getImpactLevelSrc).reversed()).collect(Collectors.toList());
}
if (multiResultListMap.containsKey(optionTypeEnum.getCode())) {
//单选+多选
List<QcDiffItemsInfo.ItemsDetail> multiItemsDetail = multiResultListMap.get(optionTypeEnum.getCode());
if (!CollectionUtils.isEmpty(multiItemsDetail)) {
List<QcDiffItemsInfo.ItemsDetail> itemsDetails = multiItemsDetail.stream().sorted(Comparator.comparing(QcDiffItemsInfo.ItemsDetail::getImpactLevelSrc).reversed()).collect(Collectors.toList());
itemsDetailList.addAll(itemsDetails);
}
}
Map<Integer, MutableTriple<Integer, Integer, Integer>> levelNumMap = compareBo.getOptionTypeDiffLevelNumMap();
if (levelNumMap.containsKey(optionTypeEnum.getCode())) {
qcDiffItemsInfo.setBadTerm(levelNumMap.get(optionTypeEnum.getCode()).getLeft());
qcDiffItemsInfo.setCoincidenceTerm(levelNumMap.get(optionTypeEnum.getCode()).getMiddle());
qcDiffItemsInfo.setGoodTerm(levelNumMap.get(optionTypeEnum.getCode()).getRight());
}
qcDiffItemsInfo.setItemsDetails(itemsDetailList);
qcDiffItemsInfos.add(qcDiffItemsInfo);
}
qcDiffDetailDTO.setQcDiffItemsInfos(qcDiffItemsInfos);
}
4 遇到的问题
在前文中,我们提到了瑕疵项验机报告的瑕疵项配置存放在阿波罗中,然而随着业务的发展,瑕疵项报告逐渐暴露出了一些问题:
- 质检项逐渐增多,导致阿波罗放不下的问题。
- 品类越来越多,每扩展一个新品类,就需要配置一整份配置,运营的工作量越来越大。
- 每扩展一个新品类,需要及时配置上,若出现延迟或者遗漏配置,会导致瑕疵项报告没法加载。
为了解决这些问题,我们采取了以下解决方案:
将瑕疵项配置从阿波罗迁移到数据库中,并创建一个后台管理系统,以方便运营人员进行配置。
此外,我们还针对未及时配置的品类制定了兜底方案:直接解析原始验机报告。
图片
图片
至此,我们已成功完成所有验机报告统一的工作。不仅如此,差异项报告和瑕疵项报告的后台管理系统也已经搭建完毕,使得在未来,即便质检标准发生调整导致质检项的增减或映射关系的变更,都能够在零成本的前提下轻松进行,无需对现有代码进行任何修改。这必将为我们的开发工作带来极大的便利和高效。
5 总结
本文详细介绍了转转C2B业务的验机报告统一流程以及差异项验机报告能力的下沉。验机报告的统一化显著减少了用户对验机报告的咨询率,极大地提升了用户体验。未来,我们将继续优化和完善系统功能,使更多的业务能够顺利接入并使用。
关于作者
方和斌,转转C2B业务研发工程师