文章

数据库实体设计交易(2.5)之退款维权

数据库实体设计交易(2.5)之退款维权

艿艿目前正在做一个开源的电商项目,胖友可以 star 下。https://gitee.com/zhijiantianya/onemall

1. 概述

本文主要分享交易模块的退款维权的数据库实体设计

基于如下信息,逆向猜测数据库实体:

【护脸旁白】笔者非电商行业出身 && 非有赞工程师,所以有错误或不合理的地方,烦请斧正和探讨。有赞是个各方面都很 NICE 的公司,推荐

2. 背景了解

2.1 买家如何申请退款/退款退货?

参见 《买家如何申请退款/退款退货?》 文档。

2.2 卖家如何处理维权订单

参见 《卖家如何处理维权订单?》 文档。

2.3 订单逆向调用

参见 《有赞云开发指南的交易场景【订单逆向调用】》 文档。

  • 打开如上文档后,搜索 “订单逆向调用 “ 关键字。

2.4 退款维权状态图

3. 数据库实体

整体实体类关系如下图:

全部实体在 Github 退款目录 下可见。

3.1 Trade

我们需要在 Trade 上,新增退款维权相关的字段。

```plain text plain /** * 交易维权状态。 * * 0 无维权, * * 1 顾客发起维权, * 2 顾客拒绝商家的处理结果, * 3 顾客接受商家的处理结果, * 9 商家正在处理; * * 101 维权处理中, * 110 维权结束; * * 备注:1到10的状态码是微信维权状态码,100以上的状态码是有赞维权状态码 */ private Integer feedback; /** * 处于交易维权状态的订单数 */ private Integer feedbackNum; /** * 退款状态。 * * 0 - NO_REFUND(无退款) * 1 - PARTIAL_REFUNDING(部分退款中) * 2 - PARTIAL_REFUNDED(已部分退款) * 3 - PARTIAL_REFUND_FAILED(部分退款失败) * 11 - FULL_REFUNDING(全额退款中) * 12 - FULL_REFUNDED(已全额退款) * 13 - FULL_REFUND_FAILED(全额退款失败) */ private Integer refundStatus;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
---

- feedback
,交易维权状态。
    - 区间 **微信交易记录**
[1, 10]
,
退款维权状态码。 // TODO 6010,猜测是微信的
的功能的投诉功能。目前暂时未测试出来。
    - 区间 **有赞**
[100, +∞)
,
退款维权状态码。
        - 101 ,退款维权处理中。当交易中存在**任意**
交易明细(订单)处于退款维权处理中时,交易则处于该状态。
        - 110 ,退款维权已结束。当交易中交易明细(订单)的退款维权**全部**
处理完成时,交易则处于该状态。
- feedbackNum
,处于退款维权状态的订单数。
    - feedback = 0 || 110
时,
feedbackNum = 0
。
    - feedback = 101
时,
feedbackNum > 0
。
- refundStatus**无退款、部分退款、全部退款**
,退款状态。分成了
的三种金额情况。
    - 为什么存在**退款中**[《订单退款到账说明》](https://help.youzan.com/qa?cat_sys=K#/menu/2118/detail/998?_k=nt1s94)
的中间状态?金额的退款不一定是实时到账,参见
文档。
    - **注意退款服务通用退款单数据与支付渠道对应退款退款维权**
,系统会存在
(不要和退款维权混淆在一起),这是一个
的服务,对接不同的支付渠道(例如,支付宝、微信、银联等等)的退款功能,并且存储
。通过这样的系统拆分解耦,不同的业务功能有
需求时,可以对接该服务。例如,现在交易里提供
功能,未来可以添加其他需要退款功能的功能。后面我们也会分享这块的设计。
- [refundedFee](https://github.com/YunaiV/doraemon-entity/blob/74600725d23eea2220490e5de07dc3556ef80036/src/main/java/cn/iocoder/doraemon/tradegroup/trade/entity/Trade.java#L317-L320)
,交易完成后退款的金额。单位:分。

## 3.2 TradeOrder

我们需要在 [TradeOrder](https://github.com/YunaiV/doraemon-entity/blob/a0b62811990444c3d3595f565bf7832ee5166e39/src/main/java/cn/iocoder/doraemon/tradegroup/trade/entity/TradeOrder.java#L99-L110) 上,新增退款维权相关的字段。

```plain text
plain /**  * 商品退款状态  *  * 0 - 无退款  * 1 - 部分退款  * 11 - 全部退款  */ private Integer refundStatus; /**  * 退款id  */ private String refundId;

  • refundStatus退款中为什么这么做呢仅仅 ,商品退款状态。不同于 Trade.refundStatus ,存在 的情况。所有退款中的情况,反馈在 Trade 记录上。 ?笔者猜测,所有支付单、退款单都 跟交易进行关联。
  • refundId至多 ,退款维权单编号,指向 TradeRefund.id 。目前有赞一个交易明细(订单) 可以发起一次退款维权。

3.3 TradeRefund

TradeRefund ,交易退款维权。

买家可以对交易下的交易明细(订单)发起退款维权,一次只能针对一个。并且,一个交易明细只允许发起一次退款维权。无论退款维权最终是成功还是失败,下一次都不能再次发起。

字段较多,我们进行简单的切块。

3.3.1 基础字段

```plain text plain /** * 编号 * * 唯一 */ private String id; /** * 店铺编号 {@link cn.iocoder.doraemon.shopgroup.shop.entity.Shop#id} */ private Integer shopId; /** * 交易编号 {@link cn.iocoder.doraemon.tradegroup.trade.entity.Trade#id} */ private String tid; /** * 交易明细编号 {@link cn.iocoder.doraemon.tradegroup.trade.entity.TradeOrder#id} */ private String oid; /** * 商品编号 {@link cn.iocoder.doraemon.tradegroup.trade.entity.TradeOrder#itemId} */ private Integer itemId; /** * 退款申请时间 */ private Date createTime; /** * 退款申请修改时间 */ private Date updateTime;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
---

- id**首字母**
,退款维权编号,唯一。例如:
“201802060008030000010077”
。笔者猜测格式为:
+ 年月日时分秒 + 自增序列(六位,每秒归零) +
0077
(可能是有什么业务含义)。
![](assets/images/learning/database/database-entity-design-trade-refund-rights/1698040340870-da18f1ab-0e44-404d-9eb3-517fd8177364.png)
- shopId
,店铺编号。
- tid
,交易编号。
- oid
,交易明细(订单)编号。
- itemId
,商品编号。

### 3.3.2 买家申请字段

界面如下:

![](assets/images/learning/database/database-entity-design-trade-refund-rights/1698040340975-21d9baa0-29c2-4a77-9696-6940cc80138b.png)

字段如下:

```plain text
plain /**  * 是否退货  *  * false - 仅退款  * true - 退货退款  */ private Boolean returnItem; /**  * 是否签收货物  *  * false - 否  * true - 是  */ private Boolean signed; /**  * 申请退款的金额,单位:分。  */ private Integer refundFee; /**  * 申请退款人的手机号码  */ private String mobile; /**  * 仅退款-未收到货申请原因  *      11(质量问题)  *      12(拍错/多拍/不喜欢)  *      3(商品描述不符)  *      14(假货), 15(商家发错货)  *      16(商品破损/少件)  *      17(其他)  * 仅退款-已收到货申  *      51(多买/买错/不想要)  *      52(快递无记录)  *      53(少货/空包裹)  *      54(未按约定时间发货)  *      55(快递一直未送达)  *      56(其他)  * 退货退款-申请原因  *      101(商品破损/少件)  *      102(商家发错货)  *      103(商品描述不符)  *      104(拍错/多拍/不喜欢)  *      105(质量问题)  *      107(其他)  */ private Integer reasonType; /**  * 买家退款申请说明  */ private String desc; /**  * 图片举证数组,以逗号分隔  */ private String images;

  • returnItem ,是否退货。交易明细(订单)处于不同状态,可以选择的选项不同:
    • 卖家未发货,可以选择 true ,不能选择退货( false )。
    • 卖家已发货,可以选择 true 和 false 。
  • signed ,是否签收货物。
  • refundFee ,申请退款的金额,单位:分。交易明细(订单)处于不同状态,可以选择的金额不同:
    • 卖家未发货,申请退款的金额只能全部
    • 卖家已发货,申请退款的金额可以 (0, 全部] 。
    • 注意交易关闭全额退款关闭 ,无论 本身处于什么状态,若退款成功的金额达到支付交易金额,交易将直接变成 状态,并标记关闭类型( Trade.closeType )为 。
    • 交易明细(订单)可退款金额的上限计算,// TODO 6011 统一写。
  • mobile ,申请退款人的手机号码。
  • reasonType相同不同 ,退款原因。根据 returnItem + signed 的组合,可选择的原因不同。并且,我们可以看到,即使原因的名字 ,对应的原因枚举编号 。
  • desc ,退款备注信息。
  • images ,图片举证数组,以逗号分隔。

3.3.3 状态字段

```plain text plain /** * 退款状态 * * 1 - WAIT_SELLER_AGREE - 买家已经申请退款,等待卖家同意 * 2 - WAIT_BUYER_RETURN_GOODS - 卖家已经同意退款,等待买家退货 * 3 - WAIT_SELLER_CONFIRM_GOODS - 买家已经退货,等待卖家确认收货 * 4 - SELLER_REFUSE_BUYER - 卖家拒绝退款 * 5 - SELLER_REFUSE_CONFIRM_GOODS - 卖家不同意,未收到货 * 10 - CLOSED - 退款关闭 * 20 - SUCCESS - 退款成功 */ private Integer status; /** * 客满介入状态 * * 1 - 客满未介入 * 2 - 客满介入中 */ private Integer csStatus; /** * 乐观锁版本号 * * 解决并发修改,防止数据不一致。 */ private Long version;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
---

- status[「 2.4 退款维权状态图 」](http://svip.iocoder.cn/Entity/trade-module-refund/#)
,退款状态。状态的变迁参见
。
- csStatus
,客服介入状态。当买家和卖家对于维权退款无法达成一致时,可以申请有赞客服介入。
- version**为什么会有该奇怪的字段呢另外,需要注意,更新时,我们需要判断更新的记录条数是否大于零**
,乐观锁版本号,使用新增或修改记录时的时间戳。
?在对退款维权记录进行操作时,在执行操作前,我们需要对记录进行各种判断以判定是否满足操作的前置条件,若满足则进行更新。等.等等..等等等…下,当我们判断满足条件时,可能已经被另外一个并发的请求已经进行修改,也就是说,此时实际已经不满足条件。那么咋办呢?我们可以通过 SQL
UPDATE trade_refund SET xxx = yyy , version = unix_timestamp(now()) WHERE id = ${id} AND version = ${version}
。即,判断是否满足操作前,我们查询退款维权记录的
version
,每次更新时,修改为当前时间戳,且判断数据库中的记录的
version
等于查询出来的
version
。
。

## 3.4 TradeRefundDelivery

界面如下:

- 卖家设置退货地址
![](assets/images/learning/database/database-entity-design-trade-refund-rights/1698040341064-ee015a02-f9a3-4085-8c54-99da2486d8ab.png)
- 买家填写退货信息
![](assets/images/learning/database/database-entity-design-trade-refund-rights/1698040341149-34567a90-b4e1-43f9-b962-18228435fcd1.png)

---

字段如下:

[TradeRefundDelivery](https://github.com/YunaiV/doraemon-entity/blob/27db54e6d4669b47afa600387bd51457cdba0b7f/src/main/java/cn/iocoder/doraemon/tradegroup/refund/entity/TradeRefundDelivery.java) ,交易退款维权的退货物流信息。

当买家选择退款并退货时,并且卖家同意退货后,买家需要提填写退货快递的物流信息。

```plain text
plain /**  * 退款编号 {@link TradeRefund#id}  */ private String id; /**  * 店铺编号 {@link cn.iocoder.doraemon.shopgroup.shop.entity.Shop#id}  */ private Integer shopId; /**  * 收件人(卖家)名  */ private String receiverName; /**  * 收件人(卖家)手机号  */ private String receiverMobile; /**  * 收件人(卖家)座机  */ private String receiverTelephone; /**  * 收件人(卖家)收获地区编号  */ private Integer receiverRegionId; /**  * 收件人(卖家)收获地址  */ private String receiverAddress; /**  * 发件人(买家)手机号  */ private String senderMobile; /**  * 发货备注  */ private String desc; /**  * 图片举证数组,以逗号分隔  */ private String images; /**  * 物流公司编号 {@link cn.iocoder.doraemon.tradegroup.delivery.entity.Express#id}  */ private Integer expressId; /**  * 物流运单编号  */ private String nu;

  • id ,退款编号,指向对应的 TradeRefund , 1 : 1 。
    • 卖家在收到买家填写的退货物流信息可以拒绝,此时买家买家可以重填退货物流信息。如果想要记录每一次买家填写的退货物流信息,可以有如下几种方式:
      1. 拼接买家填写的物流信息成字符串,记录到 TradeRefundMessage 中的信息( content )字段。
      2. 增加 TradeRefundDelivery 对应的 Log 记录表。
      3. 【建议】相对来说,第二种比第一种更加结构化。
  • shopId ,店铺编号。
  • 收件人(卖家)相关字段
    • 名字 : receiverName
    • 联系方式 : receiverMobile / receiverTelephone
    • 地址 : receiverRegionId / receiverAddress
  • 发件人(买家)相关字段

3.5 TradeRefundMessage

界面:


字段如下:

TradeRefundMessage ,交易退款维权的协商记录。

```plain text plain /** * 记录编号,唯一。 */ private Integer id; /** * 退款编号 {@link TradeRefund#id} */ private String refundId; /** * 店铺编号 {@link cn.iocoder.doraemon.shopgroup.shop.entity.Shop#id} */ private Integer shopId; /** * 发起角色 * * 1 - 系统 * 2 - 买家 * 3 - 商家 * 4 - 客服 */ private Integer ownerRole; /** * 具体操作 * * 例如: * 250 - 同意退款给买家,本次维权结束 * 206 - 已退货,等待商家确认收货 * 205 - 已同意退款申请,等待买家退货 * 201 - 发起了退款申请,等待商家处理 */ private Integer op; /** * 操作前状态 */ private Integer beforeStatus; /** * 操作后状态 */ private String afterStatus; /** * 明细数组字符串 * * JSON 格式,数组,每个元素为 {@link Detail} */ private String details; /** * 图片地址数组字符串 */ private String picURLs; /** * 创建时间 */ private Date createTime;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
---

- id
,记录编号,唯一。
- refundId
,退款编号,指向
TradeRefund.id
。
- shopId
,店铺编号。
- ownerRole**系统**[《维权订单未处理会自动退款?》](https://help.youzan.com/qa?cat_sys=K#/menu/2118/detail/1209?_k=7djbq6)
,发起角色。其中,”
“ 这个角色比较特殊,例如
。
- op
,具体操作枚举序号。
- beforeStatus
,操作前的退款维权状态。
- afterStatus
,操作后的退款维权状态。
- details**数组**[Detail](https://github.com/YunaiV/doraemon-entity/blob/27db54e6d4669b47afa600387bd51457cdba0b7f/src/main/java/cn/iocoder/doraemon/tradegroup/refund/entity/TradeRefundMessage.java#L10-L24)
,明细
字符串。JSON 格式化,数组,每个元素为
。代码如下:

```plain text
plain public static class Detail {      /**      * 标题      */     private String title;     /**      * 内容      */     private String content;  }

```plain text

  • 上面界面对应的每一个项(例如,退款原因、处理方式等等),对应一个 Detail 对象。

    ```

  • picURLS :图片地址数组字符串。

4. API

基于如下整理 API 类。

4.1 TradeRefundAPI

TradeRefundAPI退款维权 API 。

友情提示,实际场景下,API 会分拆成买家和卖家两个 API 类。

666. 彩蛋

其他和交易退款维权有关系的文档如下:

业务比较复杂,所以文档较多。


目前有赞提供 主动退款 功能,界面如下:

// TODO 6012 主动退款,未来补充

本文由作者按照 CC BY 4.0 进行授权