大继的基础级业务实战设计记录(一),积分

我发现外面对技术的文章比较多,对业务设计描述比较少,我决写把我多年功力借着不出局作业,每个模块每周写一篇和大家探讨。

一,目的

本文章目的是为了可以给正在设计积分模块的程序猴们有一个参考,把我积分进化的版本进行了一翻描述。
当然也是为了让自己能把经验以文字的方式保存下来,提升自己写作能力。

二,设计概要

  • 通用性,可用于多种类型和模块积分扩展,多种积分记录及描述,例如,用户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直接来,用终极结局方案呢,是由于有些时候系统比较简易,使用当一数据库已经可以满足大部分场景了,不需要把技术栈搞得那么花巧,难以维护.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容