轉(zhuǎn)轉(zhuǎn)C2B驗(yàn)機(jī)報(bào)告演進(jìn)之路
1 問(wèn)題背景
1.1 什么是驗(yàn)機(jī)報(bào)告?
眾所周知,轉(zhuǎn)轉(zhuǎn)是一個(gè)官方驗(yàn)的二手交易平臺(tái),對(duì)于用戶(hù)在其平臺(tái)上交易的商品,均會(huì)由專(zhuān)業(yè)的質(zhì)檢工程師進(jìn)行驗(yàn)證。這項(xiàng)驗(yàn)證將會(huì)產(chǎn)生一份詳盡的報(bào)告,這就是所謂的驗(yàn)機(jī)報(bào)告。
1.2 為什么要做驗(yàn)機(jī)報(bào)告的統(tǒng)一?
對(duì)于C2B線上回收來(lái)說(shuō),質(zhì)檢部門(mén)提供的驗(yàn)機(jī)報(bào)告具有原始性,其中可能包含一些專(zhuān)業(yè)術(shù)語(yǔ)。如果直接展示給用戶(hù),用戶(hù)可能會(huì)感到困惑。因此我們都會(huì)對(duì)驗(yàn)機(jī)報(bào)告進(jìn)行一層包裝再呈現(xiàn)給用戶(hù)。
早期C2B線上回收給用戶(hù)展示的驗(yàn)機(jī)報(bào)告是瑕疵項(xiàng)報(bào)告,這些報(bào)告在手機(jī)品類(lèi)和數(shù)碼品類(lèi)的展示方式也存在差異。此外,客服人員所能查看的用戶(hù)驗(yàn)機(jī)報(bào)告與用戶(hù)自己所看到的報(bào)告也存在差異。再后來(lái)隨著差異項(xiàng)報(bào)告的加入,就更加混亂了。同一個(gè)商品,C2B存在多份驗(yàn)機(jī)報(bào)告,驗(yàn)機(jī)報(bào)告的鏈接也不一樣。為了解決這個(gè)問(wèn)題,我們推動(dòng)了驗(yàn)機(jī)報(bào)告的統(tǒng)一。
2 C2B用戶(hù)驗(yàn)機(jī)報(bào)告的演進(jìn)
2.1 瑕疵項(xiàng)報(bào)告
瑕疵項(xiàng)報(bào)告將質(zhì)檢結(jié)果中的優(yōu)點(diǎn)和問(wèn)題逐一列舉,以供用戶(hù)查閱。判斷質(zhì)檢項(xiàng)的優(yōu)劣是根據(jù)我們自身的阿波羅配置進(jìn)行的。阿波羅的配置工作由運(yùn)營(yíng)人員負(fù)責(zé)維護(hù),如果質(zhì)檢側(cè)出現(xiàn)了新的判定屬性,就需要同步更新阿波羅的配置。瑕疵項(xiàng)驗(yàn)機(jī)報(bào)告的效果如下圖所示:
圖片
2.2 差異項(xiàng)報(bào)告
差異項(xiàng)報(bào)告通過(guò)比較用戶(hù)選擇的內(nèi)容與驗(yàn)機(jī)報(bào)告中的實(shí)際內(nèi)容,可以體現(xiàn)出偏好、偏差,或者一致性。將用戶(hù)選擇的內(nèi)容與實(shí)際檢測(cè)結(jié)果并排展示,可以使用戶(hù)一目了然,從而提升用戶(hù)體驗(yàn)。如何將實(shí)際質(zhì)檢項(xiàng)與用戶(hù)選擇的項(xiàng)映射起來(lái),判定結(jié)果好壞與否,這個(gè)數(shù)據(jù)我們是維護(hù)在數(shù)據(jù)庫(kù)中,同時(shí)也做了對(duì)應(yīng)的后臺(tái)交給運(yùn)營(yíng)去配置。差異項(xiàng)驗(yàn)機(jī)報(bào)告的效果如下圖所示:
圖片
2.3 驗(yàn)機(jī)報(bào)告統(tǒng)一
所謂驗(yàn)機(jī)報(bào)告的統(tǒng)一,意味著前后端都進(jìn)行了一致的優(yōu)化。前端頁(yè)面上的驗(yàn)機(jī)報(bào)告入口鏈接已經(jīng)得到了一致的處理,后端驗(yàn)機(jī)報(bào)告接口也得到了統(tǒng)一的收口管理。通過(guò)采用不同的策略,不同的商品能夠匹配到相應(yīng)的報(bào)告類(lèi)型。經(jīng)過(guò)多次實(shí)驗(yàn)驗(yàn)證,我們確定了優(yōu)先級(jí)順序:差異項(xiàng)報(bào)告 > 瑕疵項(xiàng)報(bào)告 > 兜底報(bào)告。
圖片
圖片
3 差異項(xiàng)報(bào)告系統(tǒng)設(shè)計(jì)
差異項(xiàng)驗(yàn)機(jī)報(bào)告對(duì)比了用戶(hù)預(yù)估報(bào)告和實(shí)際驗(yàn)機(jī)報(bào)告,很多業(yè)務(wù)都會(huì)有用戶(hù)和實(shí)際質(zhì)檢兩份報(bào)告,對(duì)比這兩份報(bào)告是一個(gè)通用的能力,因此我們對(duì)差異項(xiàng)報(bào)告能力做了下沉。
3.1 對(duì)比映射模板
對(duì)比映射模板本質(zhì)上是實(shí)際質(zhì)檢項(xiàng)與用戶(hù)預(yù)估項(xiàng)之間的映射關(guān)系。由于實(shí)際質(zhì)檢的項(xiàng)數(shù)可能遠(yuǎn)多于用戶(hù)選擇的項(xiàng)數(shù),并且實(shí)際質(zhì)檢中的許多項(xiàng)需要映射到用戶(hù)選擇的某一項(xiàng),因此我們需要配置這些項(xiàng)之間的映射規(guī)則。
- 項(xiàng)比較多:使用Excel表格來(lái)進(jìn)行數(shù)據(jù)管理,并將其存儲(chǔ)到數(shù)據(jù)庫(kù)中。
- 品類(lèi):以品類(lèi)為維度進(jìn)行區(qū)分,針對(duì)不同品類(lèi)配置不同模板。
- 特殊情況:同品類(lèi)下可針對(duì)特殊機(jī)型配置單獨(dú)的模板。
3.2 業(yè)務(wù)配置
在映射模板配置完成后,進(jìn)一步進(jìn)行業(yè)務(wù)線的配置變得必要。由于預(yù)質(zhì)檢和實(shí)際質(zhì)檢是兩個(gè)獨(dú)立的業(yè)務(wù)線,必須明確指定業(yè)務(wù)線,以確保映射關(guān)系能夠正確對(duì)應(yīng)。此外,加入業(yè)務(wù)線的配置也為未來(lái)其他業(yè)務(wù)的接入提供了方便。
圖片
3.3 映射解析
在差異項(xiàng)配置完成后,當(dāng)符合條件的商品出現(xiàn)時(shí),驗(yàn)機(jī)報(bào)告會(huì)根據(jù)我們預(yù)先配置的模板進(jìn)行解析。
/**
* 對(duì)比報(bào)告 A' 和 B
* 1.設(shè)備屬性信息對(duì)比
* 2.單選項(xiàng)對(duì)比
* 3.多選項(xiàng)對(duì)比
* 4.根據(jù)選項(xiàng)類(lèi)別分組,單選項(xiàng)先按照A價(jià)格影響因子倒序+多選項(xiàng)的排序結(jié)果
* 5.組合標(biāo)簽標(biāo)題
*/
public void compareReport(QcInfoAfterMappingBo qcInfoAfterMappingSrcBo, QcInfoAfterMappingBo qcInfoTargetBo,QcDiffTemplateBo qcDiffTemplateBo, QcDiffDetailDTO qcDiffDetailDTO) {
//B報(bào)告到A報(bào)告的映射
Map<String, List<QcDiffItemsMapping>> targetToSrcMap = qcDiffTemplateBo.getQcDiffItemsMappings().stream().collect(Collectors.groupingBy(QcDiffItemsMapping::getValueIdTarget));
//A報(bào)告 valueId -> qcItemMap
Map<String, QcInfoAfterMappingBo.ItemInfo> qcSrcValueIdMap = qcInfoAfterMappingSrcBo.getNoMappingItemInfoList().stream().collect(Collectors.toMap(QcInfoAfterMappingBo.ItemInfo::getValueId,Function.identity()));
//B報(bào)告基本信息映射 valueIdTarget -> targetConfigMap
Map<String, QcDiffTargetConf> targetConfMap = qcDiffTemplateBo.getDiffTargetConfMap();
//以A'為模板來(lái)對(duì)比
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();
//設(shè)備屬性對(duì)比
deviceInfoCompare(qcInfoAfterMappingSrcBo, qcInfoTargetBo, qcDiffDetailDTO);
//單選結(jié)果組合
singleSelectCompare(singleSelectMap, targetConfMap, targetItemMap, targetToSrcMap, qcSrcValueIdMap, compareBo);
//多選結(jié)果組合
Map<String, List<QcInfoAfterMappingBo.ItemInfo>> multiSelectMap = qcInfoAfterMappingSrcBo.getMappingMultiSelectMap();
multiSelectCompare(multiSelectMap,targetItemMap, targetToSrcMap, qcSrcValueIdMap, targetConfMap, compareBo);
//根據(jù)組合結(jié)果中報(bào)告類(lèi)型分組
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())) {
//組合標(biāo)簽內(nèi)容
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 遇到的問(wèn)題
在前文中,我們提到了瑕疵項(xiàng)驗(yàn)機(jī)報(bào)告的瑕疵項(xiàng)配置存放在阿波羅中,然而隨著業(yè)務(wù)的發(fā)展,瑕疵項(xiàng)報(bào)告逐漸暴露出了一些問(wèn)題:
- 質(zhì)檢項(xiàng)逐漸增多,導(dǎo)致阿波羅放不下的問(wèn)題。
- 品類(lèi)越來(lái)越多,每擴(kuò)展一個(gè)新品類(lèi),就需要配置一整份配置,運(yùn)營(yíng)的工作量越來(lái)越大。
- 每擴(kuò)展一個(gè)新品類(lèi),需要及時(shí)配置上,若出現(xiàn)延遲或者遺漏配置,會(huì)導(dǎo)致瑕疵項(xiàng)報(bào)告沒(méi)法加載。
為了解決這些問(wèn)題,我們采取了以下解決方案:
將瑕疵項(xiàng)配置從阿波羅遷移到數(shù)據(jù)庫(kù)中,并創(chuàng)建一個(gè)后臺(tái)管理系統(tǒng),以方便運(yùn)營(yíng)人員進(jìn)行配置。
此外,我們還針對(duì)未及時(shí)配置的品類(lèi)制定了兜底方案:直接解析原始驗(yàn)機(jī)報(bào)告。
圖片
圖片
至此,我們已成功完成所有驗(yàn)機(jī)報(bào)告統(tǒng)一的工作。不僅如此,差異項(xiàng)報(bào)告和瑕疵項(xiàng)報(bào)告的后臺(tái)管理系統(tǒng)也已經(jīng)搭建完畢,使得在未來(lái),即便質(zhì)檢標(biāo)準(zhǔn)發(fā)生調(diào)整導(dǎo)致質(zhì)檢項(xiàng)的增減或映射關(guān)系的變更,都能夠在零成本的前提下輕松進(jìn)行,無(wú)需對(duì)現(xiàn)有代碼進(jìn)行任何修改。這必將為我們的開(kāi)發(fā)工作帶來(lái)極大的便利和高效。
5 總結(jié)
本文詳細(xì)介紹了轉(zhuǎn)轉(zhuǎn)C2B業(yè)務(wù)的驗(yàn)機(jī)報(bào)告統(tǒng)一流程以及差異項(xiàng)驗(yàn)機(jī)報(bào)告能力的下沉。驗(yàn)機(jī)報(bào)告的統(tǒng)一化顯著減少了用戶(hù)對(duì)驗(yàn)機(jī)報(bào)告的咨詢(xún)率,極大地提升了用戶(hù)體驗(yàn)。未來(lái),我們將繼續(xù)優(yōu)化和完善系統(tǒng)功能,使更多的業(yè)務(wù)能夠順利接入并使用。
關(guān)于作者
方和斌,轉(zhuǎn)轉(zhuǎn)C2B業(yè)務(wù)研發(fā)工程師