我发现外面对技术的文章比较多,对业务设计描述比较少,我决写把我多年功力借着不出局作业,每个模块每周写一篇和大家探讨。
一,目的
本文章目的是为了可以给正在设计积分模块的程序猴们有一个参考,把我积分进化的版本进行了一翻描述。
当然也是为了让自己能把经验以文字的方式保存下来,提升自己写作能力。
二,设计概要
- 通用性,可用于多种类型和模块积分扩展,多种积分记录及描述,例如,用户A类积分,用户B类积分, 店铺积分。
- 简单的事务安全性,在编码时更加简单的让其事务安全。
- 建议扩展性,可以按业务需要,做出多种变化。
- 满足基础服务,转账,分账,充值,提现。
- 正确性, 系统剩余总额 = 进 - 出,可以验证,系统正确性。
三,积分余额主体设计.
- 积分主体,注需要更新时需要上锁:
- 使用 type,type_id来描述积分类型
- 例如: {type:"wallet",typeId:"1001",number:100.00,createTime:"2018-04-24 12:22:59"}
/**
* 积分账号
*/
@Entity
@Table(name = "cent")
public class Cent {
@Id
@Column(length = 37)
private String id;
/**
* 指向模块
* 例如 shop , user, wallet ,order
*/
@Column(length = 32)
private String type;
/**
* 指向模块
*/
@Column(length = 37)
private String typeId;
/**
* 当前余额
*/
@Column
private Double number;
/**
* 当前余额中锁定部分,解锁逻辑在 cent_freeze里。
*/
@Column
private Double freezeNumber;
/**
* 所拥有人,在复杂多对一业务下可为空。
* 并使own模块对拥有的人们进行描述,例如./shops/a/members , /vips/lv1/members
*/
@Column
private Long userId;
/**
* 是否可用,账号异常时可以在后台锁定。
*/
@Column
private Boolean enable;
/**
* 最后更新时间记录
*/
@Column
private Date updateTime;
/**
* 创建时间记录
*/
@Column
private Date createTime;
//省略 getter setter
}
四,变更记录设计
- 如果是简单场景,加入变更记录基本可以满足,并保留了转账,分账,充值,提现等设计的扩展。
- 这个设计需要满足后台对整个系统积分变化管理,充值管理,报表统计,异步同计等扩展。
/**
* 单体积分变更记录。
* 所有积分账变化都需要记录到这个表。
* 满足可查性需求.
* 如A转账B,这里会产生两个记录。
*/
@Entity
@Table(name = "cent_changed_log",
indexes = {@Index(columnList = "authority,permission",unique = true)})
public class CentChangedLog {
@Id
@Column(length = 37)
private String id;
/**
* 交易号。
* 本次唯一,分账和转账都由一个交易号产生多个变更日志。
* 唯一性保正,在其它实体保证,例如: CentTransfer,CentAllocation ...
*/
@Column(length = 37)
private String tradeNo;
/**
* 用设计简化,积分账号直接光联查询.
*/
@Column(length = 37)
private String centId;
/**
* 变更主体
* 用设计简化,直接使用积分类别查询.
*/
@Column(length = 37)
private String centType;
/**
* 变更主体
* 用设计简化,直接使用积分类别查询.
*/
@Column
private String centTypeId;
@Column
private String centTypeIdName;
/**
* 变更描述
*/
@Column
private String summary;
/**
* 改变数量
* 变更点数 正数,或负数来表达加或减.
*/
@Column
private Double changedNumber;
/**
* 手续费
*/
@Column
private Double changedFee;
/**
* 原交易数
* 在需要手续费的情况下这个,原交易数来记录,支付全额
* 例如: 100 -> b 扣掉2元手费, 在changeNumber里会写 98, 这个original则保留原始的数据。便于查询.
*/
@Column
private Double changedOriginalNumber;
/**
* 变更后最终余额
*/
@Column
private Double changedLeftNumber;
/**
* 变更动作
* 用于记录变理的动作,例如转账,分账充值等
*/
@Column(length = 64)
private String cause;
/**
* 变更类型,
* 如果是转账积分号,充值等记录类型来源
*/
@Column(length = 32)
private String causeType;
@Column(length = 127)
private String causeTypeId;
@Column
private String causeTypeIdName;
@Column
private Long causeUserId;
@Column
private Long userId;
/**
* 为以后特殊情况保留,正常都是 SUCCESS
*/
@Column
private String status;
@Column
private Date updateTime;
@Column
private Date createTime;
}
五,设计中的插曲
在接下来的 转账,分账等设计中,有一个统一处理的思路,然而扩展性太差而放弃,大家可以感受一下这个设计过程:
- 统一设计成一个 ChangeRequest<{transfer,charge,cashing,allocation},List<changedLogs>>
- 然后对来源进去处行描述,然后尴尬的事情发生了,无法对 分账进行描述,而且转账和充值字段也差异太大所以放弃了这个设计。
- 最终使用 transfer<List<changedLogs>> charge<List<changedLogs>> cashing<List<changedLogs>> allocation<List<changedLogs>>, 其它情况可以安需要继续扩展.
六,充值,转账,提现,锁定,分账...
- 关键约定,tradeNo, 必须全求唯一
- 我的喜欢的约定,能不关联就不关联。
- 为什么status要用String 呢,是因为用String 可读性更强,例如 以前用 1 ,2,3来标示,成功,失败,取消.由于各模块都不一样,导致每次看数据库都要去查文档,为了改变这一事实,改用 SUCCESS,FAIL,CANCEL来表达,代价是性能稍微下降,但我们有千万种方法来提高这种性能,如果系统足够赚钱时。
- 过段时间还能看得懂原则,字段和表设计实现后过段时间能不看,设计稿能看懂最好。
/**
* 积分充值
*/
@Entity
@Table(name = "cent_charge_log",
indexes = {@Index(columnList = "tradeNo",unique = true),@Index(columnList = "sourceTradeNo",unique = true)})
public class CentChargeLog {
@Id
@Column(length = 37)
private String id;
/**
* 交易ID,全求唯一
*/
@Column(length = 64)
private String tradeNo;
/**
* 充值的积分账号
*/
@Column(length = 37)
private String centId;
/**
* 充值的积分类型
*/
@Column(length = 32)
private String centType;
/**
* 充值的积分类型ID
*/
@Column(length = 37)
private String centTypeId;
/**
* 充值的积分类型ID名称
*/
@Column
private String centTypeIdName;
/**
* 所属于用户,需要对集合描述为空并用own进行描述
*/
@Column
private Long userId;
/**
* 充值数量
*/
@Column
private Double number;
/**
* 充值来源,可以是微信,支付宝,银行等第三方,或本系统现金
*/
@Column(length = 64)
private String sourceType;
/**
* 充值来源ID ,微信支付有openId,现金使用管理员ID,银行使用银行卡号
*/
@Column
private String sourceTypeId;
/**
* 充值来源ID名称
*/
@Column
private String sourceTypeIdName;
/**
* 充值来源交易号
* 需要事务保证唯一
*/
@Column
private String sourceTradeNo;
/**
* 来源交易结果
*/
@Column
private String sourceResultCode;
/**
* 状态
*/
@Column(length = 16)
private String status;
/**
* 更新时间
*/
@Column
private Date updateTime;
/**
* 创建时间
*/
@Column
private Date createTime;
}
/**
* 积分提现
*/
@Entity
@Table(name = "cent_cashing_log",
indexes = {@Index(columnList = "tradeNo",unique = true),@Index(columnList = "targetTradeNo",unique = true)})
public class CentCashingLog {
@Id
@Column(length = 37)
private String id;
/**
* 交易ID,全求唯一
*/
@Column(length = 64)
private String tradeNo;
@Column(length = 37)
private String centId;
@Column(length = 64)
private String centType;
@Column(length = 37)
private String centTypeId;
@Column
private String centTypeIdName;
@Column
private Double number;
@Column
private Double originalNumber;
@Column
private Double fee;
@Column(length = 64)
private String targetType;
@Column(length = 37)
private String targetTypeId;
@Column
private String targetTypeIdName;
@Column
private String targetTypeAccountId;
@Column
private String targetTypeAccountTypeName;
@Column
private String targetTypeAccountUserId;
@Column
private String targetTypeAccountUserName;
@Column(length = 127)
private String targetTradeNo;
@Column
private String targetResultCode;
@Column(length = 16)
private String status;
@Column
private Long userId;
@Column
private Date updateTime;
@Column
private Date createTime;
}
/**
* 积分转账
*/
@Entity
@Table(name = "cent_transfer_log",
indexes = {@Index(columnList = "tradeNo",unique = true)})
public class CentTransferLog {
@Id
@Column(length = 37)
private String id;
@Column(length = 37)
private String tradeNo;
@Column(length = 37)
private String fromCentId;
@Column(length = 37)
private String toTypeCentId;
@Column
private Double number;
@Column
private Double containFee;
@Column
private String summary;
@Column(length = 16)
private String status;
@Column
private Date updateTime;
@Column
private Date createTime;
}
/**
* 积分锁定
*/
@Entity
@Table(name = "cent_freeze")
public class CentFreeze {
@Id
@Column(length = 37)
private String id;
@Column(length = 37)
private String centId;
@Column(length = 64)
private String centType;
@Column(length = 37)
private String centTypeId;
@Column
private Long userId;
@Column
private Double number;
@Column
private String reason;
@Column(length = 16)
private String status;
@Column
private Date unfreezeTime;
@Column
private Date createTime;
}
/**
* 分账
*/
@Entity
@Table(name = "cent_allocation",
indexes = {@Index(columnList = "tradeNo",unique = true)})
public class CentAllocation {
@Id
@Column(length = 37)
private String id;
@Column(length = 37)
private String tradeNo;
@Column(length = 37)
private String fromCentId;
@Column(length = 64)
private String fromCentType;
@Column(length = 37)
private String fromCentTypeId;
@Column
private String fromCentTypeIdName;
@Column
private Double amount;
@Column(length = 16)
private String status;
@Column
private Date updateTime;
@Column
private Date createTime;
@OneToMany(fetch=FetchType.EAGER)
@JoinColumn(name="centAllocationId", referencedColumnName="id",foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT),insertable = false,updatable = false)
private Set<CentAllocationItem> centAllocationItems;
}
/**
* 分账项
*/
@Entity
@Table(name = "cent_allocation_item")
public class CentAllocationItem {
@Id
@Column(length = 37)
private String id;
@Column(length = 37)
private String centAllocationId;
@Column
private String summary;
@Column(length = 37)
private String fromCentId;
@Column(length = 64)
private String fromCentType;
@Column(length = 37)
private String fromCentTypeId;
@Column
private String fromCentTypeIdName;
@Column(length = 37)
private String toCentId;
@Column(length = 64)
private String toCentType;
@Column(length = 37)
private String toCentTypeId;
@Column
private String toCentTypeIdName;
@Column
private Double containFee;
@Column
private Double amount;
@Column
private Long freezeSecond;
@Column(length = 16)
private String status;
@Column
private Date updateTime;
@Column
private Date createTime;
}
七、简易关系统数据库扩展
- 如果为小型应用,可以直接添加 cent_daily_detail ,cent_monthly_detail 来统计积分变化情况,尽量避免无限累积,可能会超出字段最大值。
- 数据量大时,可以用hash分布数据库来提升写入,使用数据流来统计数据以免遇到性能瓶颈。
- 如果是微服务需要聚合userInfo,可以看看noSql数据聚合。
八、实际情况,及性能.
- 一个设计不能满足所有情况,同学们需要根据自己业务实际情况进行调整.
- 如果您是一个简单单体服务,可以按需求直接修改设计到满足自己的业务,这个设计提供了主要的设计思路。
- 为何不直接用 noSQL直接来,用终极结局方案呢,是由于有些时候系统比较简易,使用当一数据库已经可以满足大部分场景了,不需要把技术栈搞得那么花巧,难以维护.