接着订单说,订单结算时要给用户加款、给平台加手续费。涉及用户和资金。
用户
只有一套用户怎么设计都行,至少两张表,一张主表只存用户账密、名称等基本信息,再扩展一张表存用户实名等其他属性。
登录的时候只查主表,主表 id 作为整个系统的用户 id,不要用手机号等其他标识作为用户 id。我之前有个业务场景手机号是唯一键,于是我用手机号做业务主键,结果后来业务改成允许一个手机号多个用户,我就傻眼了。
在这些基础架构上要技术主导。
sql
-- 用户主表 - 存储基本登录信息
CREATE TABLE `user_main` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID,系统唯一标识',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码(加密存储)',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号(非唯一,允许多用户共享)',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '账户状态:1-正常,0-禁用',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_phone` (`phone`),
KEY `idx_email` (`email`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户主表-基本登录信息';
-- 用户扩展表 - 存储实名等其他属性
CREATE TABLE `user_profile` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`user_id` bigint(20) NOT NULL COMMENT '用户ID,关联user_main.id',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`id_card` varchar(18) DEFAULT NULL COMMENT '身份证号',
`gender` tinyint(1) DEFAULT NULL COMMENT '性别:1-男,2-女,0-未知',
`birthday` date DEFAULT NULL COMMENT '生日',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`address` varchar(200) DEFAULT NULL COMMENT '地址',
`company` varchar(100) DEFAULT NULL COMMENT '公司',
`position` varchar(50) DEFAULT NULL COMMENT '职位',
`bio` text DEFAULT NULL COMMENT '个人简介',
`is_verified` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否实名认证:1-已认证,0-未认证',
`verified_at` timestamp NULL DEFAULT NULL COMMENT '实名认证时间',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`),
UNIQUE KEY `uk_id_card` (`id_card`),
KEY `idx_real_name` (`real_name`),
KEY `idx_is_verified` (`is_verified`),
CONSTRAINT `fk_user_profile_user_id` FOREIGN KEY (`user_id`) REFERENCES `user_main` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户扩展表-实名等其他属性';
但有时候一个系统有多套用户,怎么设计取决于业务场景,如果业务要求一个角色在页面切换其他角色,那数据库的设计是 1 张用户主表 + 多张角色扩展表。
但是更建议使用两套独立的用户表。上面的实现方式会在业务的各个场景增加复杂度,业务发展越大,问题越多。现在随便打开一个多角色 App,比如 BOSS 直聘,招人和应聘登录都不在一起。像美团这样的,点餐的客户和跑腿的骑手都区分 App 了。
我之前做过一个业务最开始用的方式 1,随着用户相关的需求迭代,越来越难改,最后索性停止迭代了😂。
资金
资金的事先让财务设计流向链路,我之前做过一个业务,因资金流向问题,扣了大量的税,业务挣的钱基本上都交税了。
资金流向避免作为钱入平台,再从平台分账,这样流水太大,财务必定很难搞。
可以设计成担保 + 手续费模式,像咸鱼这样,钱是从买家到卖家账户,平台只收取手续费。
模式一:资金入平台模式(不推荐)
模式二:担保+手续费模式(推荐)
库表设计
sql
-- 资金账户表
CREATE TABLE fund_account (
account_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '账户ID',
account_no VARCHAR(32) NOT NULL UNIQUE COMMENT '账户编号',
account_name VARCHAR(100) NOT NULL COMMENT '账户名称',
account_type TINYINT NOT NULL DEFAULT 1 COMMENT '账户类型:1-现金账户,2-银行账户,3-支付宝,4-微信,5-其他',
user_id BIGINT NOT NULL COMMENT '用户ID',
balance DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额(可用余额)',
frozen_amount DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '冻结金额',
currency VARCHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
remark VARCHAR(255) COMMENT '备注',
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_user_id (user_id),
INDEX idx_account_no (account_no),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金账户表';
-- 资金流水表
CREATE TABLE fund_transaction (
transaction_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '流水ID',
transaction_no VARCHAR(64) NOT NULL UNIQUE COMMENT '流水号',
account_id BIGINT NOT NULL COMMENT '账户ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
transaction_type TINYINT NOT NULL COMMENT '交易类型:1-收入,2-支出,3-转入,4-转出,5-冻结,6-解冻',
amount DECIMAL(15,2) NOT NULL COMMENT '交易金额',
balance_before DECIMAL(15,2) NOT NULL COMMENT '变动前账户余额',
balance_after DECIMAL(15,2) NOT NULL COMMENT '变动后账户余额',
frozen_before DECIMAL(15,2) NOT NULL COMMENT '变动前冻结金额',
frozen_after DECIMAL(15,2) NOT NULL COMMENT '变动后冻结金额',
business_type VARCHAR(32) NOT NULL COMMENT '业务类型:recharge-充值,withdraw-提现,transfer-转账,payment-支付,refund-退款等',
business_id VARCHAR(64) COMMENT '业务单号',
counterpart_account_id BIGINT COMMENT '对方账户ID(转账时使用)',
currency VARCHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-失败,1-成功,2-处理中',
remark VARCHAR(255) COMMENT '备注',
transaction_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '交易时间',
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_account_id (account_id),
INDEX idx_user_id (user_id),
INDEX idx_transaction_no (transaction_no),
INDEX idx_business_type (business_type),
INDEX idx_business_id (business_id),
INDEX idx_transaction_time (transaction_time),
INDEX idx_status (status),
FOREIGN KEY (account_id) REFERENCES fund_account(account_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金流水表';
核心就这两张表,资金里面至少有两种账户类型,余额和冻结。
根据业务类型还可以添加更多类型,我曾做过一个业务里面除了这两个还包含分配、退回等。
账户表里有多少个分类,流水表就加多少前后快照。
任何资金操作,充值、购买、申请退款等,这两张表都是用悲观锁 + 事物包裹的,外层再加个分布式锁。分布式锁的 key 可以用用户 id,保证同一个用户同一时刻只有一个资金操作。(一般的业务场景这样都够用了,并发高就用 redis + kafka,实时操作换成,异步 kafka 入库,我之前好像写过,你可以翻翻我之前的文章。)
资金操作类型根据业务场景定,不要做通用的。比如叫冻结,在申请退款和押金的时候都用,虽然在数据库层面都是减少余额增加冻结,但不要这样写,太耦合了后期迭代搞死人。
比如我会操作类型拆为

以下是购买的流程。

如果涉及资金从 A 账户流向 B 账户,比如 A 转账给 B,那就写个转账的类型,把 A、B 的两张表放到同一个事物。
如果一个操作同时涉及余额减少和冻结增加,写在一条流水里面,不要拆分。
以上做完后数据就不可能出问题,资金的重要性大于系统,系统可以崩,资金不能乱。
对账
每一个操作完成,异步发个延时任务对账,根据不同操作,执行不同的对账逻辑。
像 AICD 一样检查操作的原子性、一致性。
通常还有个日对账,每日结束后,核对系统层面一共收了多少钱、只出了多少钱,内部扭转了多少钱,对账逻辑可以让财务出。
对账可以保证万一出问题,出问题的那一刻就能察觉。