作者:靳澤一
入職轉轉一年多,大部分時間都在負責售后業務的前端開發,本文主要從前端視角,分享一下轉轉售后的業務和系統,本文目錄如下:
從業務出發
作為電商公司,售后服務不僅僅是一個交易的結束,也是下一個交易的開始。做好售后服務,可以很好的提升用戶體驗和口碑,提高用戶的滿意度。轉轉售后項目主要有兩部分:
轉轉售后作為一個較為復雜的業務,主要包括從用戶申請售后到完成散貨的全部鏈路,下圖為一個簡單的售后流程示意圖:

售后業務的復雜除了業務流程之外,還有以下幾個方面的挑戰:
- 涉及多個運行環境:包括轉轉、找靚機、采貨俠以及小程序和 H5 頁面
- 多個業務例如:3C、圖書、游戲、奢侈品、虛擬商品等
- 多種售后類型:維修、退貨退款、換貨等

面對復雜的業務時,如何適配多端環境,兼容多種業務場景、多種售后類型,并且更快更好的應對業務變化便是我們在系統開發時需要思考的問題。
前端技術架構
先看一下轉轉售后系統的前端技術架構圖:

轉轉具有比較完善的前端技術架構,如上圖,售后系統的開發完全按照其系統架構進行開發,其中中包含了
- UI 協作:藍湖、可視化輔助平臺
- 技術棧:Vue(用戶端)、React(后臺)
- 工具庫&組件庫:zz-ui、zant-ui、多端適配器 native-adapter,call-app 喚端、zz-util 工具庫等
- 工程化:apollo 配置平臺、zz-cli 腳手架、umi-cli
- 開發調試:whistle 代理,zApi 接口管理平臺
- 監控平臺:sentry 異常監控、性能分析平臺、以及 lego 埋點
- ......
從業務到技術
日常的業務開發,我主要講一下兩個方面
用戶側頁面設計
售后場景的多樣性,導致在用戶側很多頁面雖然頁面相似,但不同客戶端、不同業務、不同狀態需要展示給用戶的信息都有差異。在需要用同一套頁面去兼容多端多業務場景時如果使用大量 if-else 做業務邏輯處理,又或者是針對每種場景都開發一套頁面都是非常麻煩的事。在面對如擴品類、業務下線等業務變化時,對于項目的影響和改動也會比較大,大大增加測試回歸成本。因此我們更多采用配置化的方式解決這個問題。
在用戶點擊進入售后頁面時,我們需要根據不同業務在售后的不同狀態節點跳轉進入不同的售后頁面。這里我們需要根據首先會進入一個空白中轉頁面,在這個頁面調用接口查詢,根據接口返回鏈接進入不同頁面,前端不需要做過多的判斷,并且在其他業務需要跳轉售后頁面時,只需要提供中轉頁面的鏈接即可。
對于售后類型選擇頁面,我們會在后臺針對商品不同品類、業務線、客戶端、申請時效等配置頁面需要展示的售后服務類型。并且關聯不同售后類型下的原因配置。

在售后申請頁面,對于不同的售后類型,售后業務,需要用戶填寫的信息以及表單的交互邏輯都有不同。頁面如下:

我們采用數據驅動視圖的方式完成頁面邏輯和表單渲染,首先和后端定義表單的設計,根據不同場景在 ?Apollo 配置平臺配置多種售后申請表單,表單配置示例如下圖所示:
"formInfo": [
{
"tip": "",// 提示信息
"title": "收貨狀態",// 表單名稱
"placeholder": "",// placeholder信息
"type": "",//組件類型(例如對于輸入框需要區分是普通輸入框還是textarea)
"componentRef": "refname",// 組件ref值/表單key值(唯一)
"componentName": "componentName",// 組件名(同一個表單可能會出現多個同名組件)
"options": [
{
"id": "1",
"name": "我已收到貨",
"nonRequiredComponentNames": "unlock", // // 聯動信息:選擇當前選項之后需要隱藏的組件(配置componentRef)
"isDefault": "", // 是否是默認值
"children": [], // 子組件
"requiredFields": [// 聯動組件ref以及option
{
"requiredRef": "reasonId",
"requiredOptions": ""
}
]
}
],
"rules": [ // 表單組件校驗規則
{
"name": "isRequire",
"value": "1",
"message": "收貨狀態必填",
"messageType": "alert"
}
]
}
]
配置信息中包含表單渲染需要的所有信息以及規則,另外用戶須知等一些文案展示信息也會一起配置。在前端項目中,對頁面進行組件拆分,根據接口獲取的配置信息渲染頁面。代碼如下:
<div v-for=“(formItemInfo, index) in formInfo” :key=“index”>
// 通用組件手
<form-item-action-sheet
v-show="!formItemInfo.needHide"
:key="formItemInfo.componentRef"
:ref="formItemInfo.componentRef"
:formItemInfo="formItemInfo"
@change="onActionSheetChange"
/>
</div>
在售后申請頁面,我們需要做好信息觸達,我們會在后臺配置用戶側信息展示的配置,使得不同業務、不同狀態的售后可以給用戶展示相應的信息。

在配置平臺,可以根據業務類型配置用戶側展示的流水信息,售后節點信息、推送等。
最后,由于各種配置較多,為了方便使用,開發了配置校驗工具,通過配置校驗工具可以對上面所有的配置進行校驗,提高效率和配置的準確性。

這種多種配置化相結合的方式,對于轉轉售后這種趨于成熟穩定的售后流程而言,具有很多優勢
一些簡單的頁面信息以及表單邏輯的修改,產品可以直接修改配置信息完成,不需要進行開發并上線
擴品類時,我們只需要新增個別組件并且按照相同的模式配置表單,由后端查詢返回即可,大大減少前端開發的工作量
后臺系統
售后后臺系統采用 React + Hooks + unstated-next 技術棧,全面擁抱 Function 組件寫法,林語堂說過:"懶惰使人進步”,為了更快更好的完成日常工作,有更多的時間“摸魚“。我們就需要提高開發效率,在盡量短的時間內完成工作。
為了方便使用系統的售后人員工作,對于系統中的表格均采用如下方式展示信息

對于這種系統中很多這種重復的篩選表單+表格的形式,我們會進行組件封裝,將售后系統中的組件按表單、表格、彈窗、視圖以及自定義 hook 等進行封裝,

基于 useAntdTable 實現,在原有功能基礎上結合售后業務邏輯封裝自定義 Hook,對于接口輸入和輸出進行格式化,對于分頁邏輯,篩選表單邏輯,刷新頁面等邏輯,實現售后系統中篩選表單的邏輯復用;使用時只需要傳入相應的配置信息,并把返回的 table props 和 filter props 傳給對應的 表格 和 表單 組件,達到表單頁面的配置化開發。
export default (requestApi, option = {}) => {
const { title, filterConfig = [], wrapperParams = {}, getColumns, ...options } = option
// ......省略部分代碼
const getTableData = useCallback(
(params, formData) => {
const { current, pageSize, sorter: s = {}, filters: f = {} } = params
// 處理getTableData返回的表格的屬性和方法
// 過濾掉篩選表單中的null、undefined空值,''和 0不會過濾
const filterNullObj = objFilter(
formData || {},
(_, value) => value !== null && value !== undefined
)
// ......省略部分代碼
return requestApi({
...wrapperParams,
...p,
...filterNullObj
}).then((res = {}) => ({
total: +res.totalCount || 0,
list: res.dataList || res.ticketDownloadTasks || []
}))
},
[requestApi, wrapperParams]
)
const result = useAntdTable(getTableData, {
defaultPageSize: 5,
form,
...options
})
const { refresh } = result
const { submit } = result.search
const columns = useMemo(() getColumns && getColumns(refresh, wrapperParams), [
wrapperParams,
refresh,
getColumns
])
result.search = {
...result.search,
// 返回篩選框的配置信息
filterConfig: typeof filterConfig === 'function' ? filterConfig(submit) : filterConfig,
form
}
return result
}
對于篩選表單封裝比較簡單,通過遍歷 filterConfig 配置信息并傳給 Form.Item,內部封裝表單聯動,搜索、重置等功能邏輯,使開發可以變成配置化。后續這種業務情景如果比較多且沒有復雜聯動,會繼續優化,采用內置組件類型,通過后端驅動篩選表單的方式。
對于售后工作人員來說,“「時間就是生命,效率就是金錢」”,效率是他們衡量售后系統優良最重要的標準,除了表格上信息的高度集合之外,對于售后詳情頁面,也會盡可能多的將所需要的信息展示給工作人員, 并且增加一些自動化設計,來減少售后人員操作,提高效率。

在售后詳情頁中,通過多個 tab 展示更多的信息,在當不同崗位的售后工程師通過不同入口進入詳情時,會直接直接定位至對應的 tab 下。另外還對 tab 的操作方式進行了修改,當鼠標懸浮在 tab 上時切換,點擊時會刷新當前 tab 信息,方便工程師在詳情頁的頻繁操作時的效率。
對于售后收貨人員操作收貨本質是一個比較同質化流水線操作,但是輸入框的聚焦、選中、搜索、清空、按鈕點擊等人機交互會降低他們整體的審核效率。基于這問題內置聚焦 + 自動請求來簡化收貨人員操作。主要會有以下幾個訴求:
聚焦
- 進入頁面聚焦
- 切回瀏覽器頁簽 - 聚焦選中
- 切換系統頁簽/重新滾動到可視化區域時- 聚焦選中
- 請求數據/提交表單后 - 聚焦選中
請求模式
- 打標模式 - 防抖 200ms 自動請求或按下 Enter 自動請求
- 輸入模式 - 本身不會自動請求只有當按下 Enter 鍵才請求
組件封裝如下:
const FastInput = React.forwardRef(({supportBatch, ...otherProps}, ref) => {
const [mode, setMode] = useState('print') // print 打標機模式 edit 手動錄入模式
// ......省略部分代碼
// 將當前輸入框聚焦并選中
const selectAll = usePersistFn(() {
inputRef.current.focus({
cursor: 'all'
})
})
// 監聽輸入框是否可見,不可見-> 可見則需要聚焦且選中文本
const inViewPort = useInViewport(wrapRef)
const { run: printUpdateForm, flush, cancel } = useDebounceFn(
(value) => {
otherProps.onSubmit ? otherProps.onSubmit(value) : otherProps.onChange(value)
// 在每次出發提交方法之后,再次全選,減少用戶操作
;inputRef.current.focus({
cursor: 'all'
})
},
{ wait: 200 }
)
// 手寫模式,按回車才更新表單
const editUpdateForm = (value) => {
// 同上,調用提交方法,并選中
// ......省略部分代碼
}
// 監聽所在瀏覽器頁簽是否可見 -切換為所在頁簽自動聚焦并選中
useEffect(() {
inputRef.current.focus()
const revalidate = () {
if (!isDocumentVisible()) return
selectAll()
}
if (typeof window !== 'undefined' && window.addEventListener){
window.addEventListener('visibilitychange', revalidate, false)
}
return () {
window.removeEventListener('visibilitychange', revalidate, false)
}
}, [selectAll])
// 監聽所在輸入框時候位于可見區域,當移動到可見區域時,自動聚焦并選中
useUpdateEffect(() {
if (inViewPort) selectAll()
}, [inViewPort])
// 只有打標模式才通過防抖通知父元素
const onChange = usePersistFn((e) => {
// ......省略部分代碼
if (mode === 'print') {
printUpdateForm(values)
}
})
// 監聽 Enter鍵- 打標模式下當前防抖立即調用;輸入模式下則直接通知父元素更新
const onPressEnter = usePersistFn((e) => {
if (mode === 'print') {
flush()
}
if (mode === 'edit') {
editUpdateForm(e.target.value)
}
})
// 切換模式;取消當前防抖
const transform = (mode) => {
if (mode === 'print') {
cancel()
}
inputRef.current.focus()
setMode(mode)
}
return (
<span ref={wrapRef}>
<CompInput
ref={inputRef}
{...otherProps}
notallow={onPressEnter}
value={value}
notallow={onChange}
suffix={
// 輸入框后面的icon以及點擊切換mode
// ......省略部分代碼
}
/>
</span>
)
})
寫在最后
最后,雖然目前轉轉售后流程比較完善和穩定,但仍然存在一些業務痛點,需要我們持續思考、優化:
售后作為電商公司的“護城河”,在售后業務中,滿意度是一個很重要的衡量指標。如何提升滿意度也成了一個非常重要的問題。除了做好日常開發,保證項目質量之外,我們還需要思考如何通過技術手段去提升用戶體驗,分析用戶行為,尋找差評原因。
在涉及多個業務,多種售后類型之后,售后流程變得十分復雜,多個流程的耦合導致業務變動時,測試回歸的工作很麻煩,因此,在系統設計和開發的時候,也需要考慮如何通過技術設計來減少測試成本。
如何提升后臺系統的操作效率 作為一個業務開發,我們需要衡量好業務開發與技術創新的關系,技術服務于業務,來創造價值,完全脫離業務的技術創新就是在“耍流氓”。完成基本業務開發的同時,我們需要思考如何通過技術手段解決業務痛點,來達到技術創新的目的。