当前位置:网站首页>业务表解析-余额系统
业务表解析-余额系统
2022-08-03 05:07:00 【DBCai】
业务表解析-余额系统
业务要求
- 有个地方可以查看用户的 可用余额 与 冻结余额
- 还有个地方可以查看用户余额(可用余额 + 冻结余额)变动的明细
- 后台可以查看用户余额变动明细,可通过类型,变更类型,甚至备注去匹配记录
业务例子
- 有那么一种关系,下级购物,上级可以获得佣金
- 当下级购物并且付款后,上级立马获得冻结余额。具体实现就是给上级创建一条增加冻结余额的记录,然后增加上级的冻结余额
- 当下级确认收货,并且订单得到结算后,就需要将冻结余额转移到可用余额中。具体实现就是给上级创建一条减少冻结余额的记录,然后上级减少冻结余额。接着,给上级创建一条添加可用余额的记录,然后增加上级的可用余额
表结构设计(mysql)
//可用余额 与 冻结余额
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`balance` decimal(12,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '可用余额',
`frozen_balance` decimal(12,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '冻结余额',
PRIMARY KEY (`id`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
//余额记录表
CREATE TABLE `user_balance_records` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`status` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '余额状态 1=可用余额 2=冻结余额',
`targetable_type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '目标类型',
`targetable_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标模型ID',
`type` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '类型',
`change_action` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '变更类型 1=增加 2=减少',
`change_value` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '变更值',
`old_value` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '旧值',
`new_value` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '新值',
`comment` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
`finance_comment` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '财务备注',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_balance_records_user_id_index` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户余额流水表';
字段解释
- users.balance:可用余额,用来统计可以使用的余额,≥ 0
- users.frozen_balance:冻结余额,用来统计冻结的余额,≥ 0
- user_balance_records.status:入账状态,1=可用余额,2=冻结余额
- user_balance_records.targetable_type 与 user_balance_records.targetable_type_id, 对应引起余额变化的目标模型。例子:下单使用了余额,这里就是订单模型 与 订单ID了
- user_balance_records.type:类型,必须要足够精确。使用余额下单是一种类型,订单取消后,退还也是一种类型,使用余额发红包是一种,领取别人的红包是一种,因未领取完而回退的红包金额也是一种类型。这里,可能有人会问,既然已经有了目标模型,难道还无法确认什么类型吗?可以,但是不直观。例子:如果开发人员把充值与退款所消耗的余额都记录到了一条记录上(暂不考虑设计合理与否),此时仅由记录(targetable_type + targetable_type_id) 来识别的话,我们是无法一眼就看出这条记录是因为充值还是退款引起了,后期做搜索也不好做
- user_balance_records.change_action:值的变更类型,正数就是 增加 ,负数就是减少,冗余做筛选用的
- user_balance_records.change_value:变更值,负数时带符号
- user_balance_records.old_value:旧值,也就是变更前的 users.balance 或者 users.frozen_balance
- user_balance_records.new_value:新值,也就是变更后的 users.balance 或者 users.frozen_balance
- user_balance_records.comment:备注
- user_balance_records.finance_comment 财务备注,部分公司需要
程序思考
- 从表设计中可以发现,每一次余额的变动,都会修改 user_balance_records 与 users 表,所需存储记录时,必须加上事务
- 为了余额的准确性,必须先获得锁后,才能进行余额的相关操作
以此为界限,下面是 laravel 中的具体实现
数据表迁移文件
//给用户表添加余额字段的 up 方法(原本就存在了users 表,所有里面没有ID)
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedDecimal('balance', 12)->default('0.00')->comment('余额');
$table->unsignedDecimal('frozen_balance', 12)->default('0.00')->comment('冻结余额');
});
}
//创建余额记录表的 up 方法
public function up()
{
Schema::create('user_balance_records', function (Blueprint $table) {
$table->increments('id');
$table->unsignedBigInteger('user_id')->default('0')->comment('用户ID')->index();
$table->unsignedTinyInteger('status')->default('1')->comment('入账状态 1=可用余额,2=冻结余额');
$table->string('targetable_type')->default('')->comment('目标类型');
$table->unsignedBigInteger('targetable_id')->default('0')->comment('目标模型ID');
$table->unsignedTinyInteger('type')->default('1')->comment('类型');
$table->unsignedTinyInteger('change_action')->default('1')->comment('变更类型 1=增加 2=减少');
$table->decimal('change_value', 12)->default('0.00')->comment('变更值');
$table->decimal('old_value', 12)->default('0.00')->comment('旧值');
$table->decimal('new_value', 12)->default('0.00')->comment('新值');
$table->string('comment')->default('')->comment('备注');
$table->string('finance_comment')->default('')->comment('财务备注');
$table->timestamps();
$table->comment('用户余额流水表');
});
}
UserBalanceRecord 模型类
<?php
namespace App\Models\User;
use App\Models\User;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
/** * App\Models\User\UserBalanceRecord * * @property int $id * @property int $user_id 用户ID * @property int $status 入账状态 1=可用余额,2=冻结余额 * @property string $targetable_type 目标类型 * @property int $targetable_id 目标模型ID * @property int $type 类型 * @property int $change_action 变更类型 1=增加 2=减少 * @property string $change_value 变更值 * @property string $old_value 旧金额 * @property string $new_value 新金额 * @property string $comment 备注 * @property string $finance_comment 财务备注 * @property Carbon|null $created_at * @property Carbon|null $updated_at * @property User|null $user */
class UserBalanceRecord extends Model
{
use HasDateTimeFormatter;
protected $table = 'user_balance_records';
protected $guarded = [];
//改变类型
public const CHANGE_TYPE_INC = 1;
public const CHANGE_TYPE_DEC = 2;
public const CHANGE_TYPE_MAP = [
self::CHANGE_TYPE_INC => '增加',
self::CHANGE_TYPE_DEC => '减少'
];
//入账状态 1=可用余额,2=冻结余额
public const STATUS_ALREADY = 1;
public const STATUS_WAIT = 2;
public const STATUS_MAP = [
self::STATUS_ALREADY => '可用余额',
self::STATUS_WAIT => '冻结余额'
];
/** * 类型:根据改变余额的类型,自行添加 */
public const TYPE_RED_ENVELOPE_SEND = 1;
public const TYPE_RED_ENVELOPE_RECEIVE = 2;
public const TYPE_MAP = [
self::TYPE_RED_ENVELOPE_SEND => '发红包',
self::TYPE_RED_ENVELOPE_RECEIVE => '领红包',
];
public function targetable()
{
return $this->morphTo();
}
public function user()
{
return $this->belongsTo(User::class);
}
}
UserBalanceRecordService 类
<?php
namespace App\Services\User;
use App\Constants\CacheKey;
use App\Models\User;
use App\Models\User\UserBalanceRecord;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class UserBalanceRecordService
{
/** * 修改余额 * * @param Model $model * @param int $userId * @param float $changeValue * @param int $type 看模型 * @param string $comment * * @return bool */
public static function changeBalance(
Model $model,
int $userId,
float $changeValue,
int $type,
string $comment = ''
): bool {
$lock = Cache::lock(CacheKey::LOCK . 'user-balance:' . $userId, 10);
try {
// 为获得锁等待最多 5 秒...
$lock->block(5);
$isNaturalNumber = bccomp((string)$changeValue, '0.00', 2) !== -1;
$changRow = User::query()
->where('id', $userId)
->when(!$isNaturalNumber, function ($query) use ($changeValue) {
$query->where('balance', '>=', abs($changeValue));
})
->increment('balance', $changeValue);
if ($changRow === 0) {
return false;
}
$newValue = User::query()->where('id', $userId)->value('balance') ?? '0.00';
//添加记录
$record = new UserBalanceRecord();
$record->user_id = $userId;
$record->type = $type;
$record->change_value = $changeValue;
$record->old_value = bcsub($newValue, (string)$changeValue, 2);
$record->new_value = $newValue;
$record->change_action = $isNaturalNumber ? UserBalanceRecord::CHANGE_TYPE_INC : UserBalanceRecord::CHANGE_TYPE_DEC;
$record->comment = $comment;
$record->status = UserBalanceRecord::STATUS_ALREADY;
$record->targetable()->associate($model);
return $record->save();
} catch (\Exception $e) {
return false;
}
finally {
optional($lock)->release();
}
}
/** * 修改冻结余额 * * @param Model $model * @param int $userId * @param float $changeValue * @param int $type 看模型 * @param string $comment * * @return bool */
public static function changeFrozenBalance(
Model $model,
int $userId,
float $changeValue,
int $type,
string $comment = ''
): bool {
$lock = Cache::lock(CacheKey::LOCK . 'user-frozen-balance:' . $userId, 10);
try {
// 为获得锁等待最多 5 秒...
$lock->block(5);
$isNaturalNumber = bccomp((string)$changeValue, '0.00', 2) !== -1;
$changRow = User::query()
->where('id', $userId)
->when(!$isNaturalNumber, function ($query) use ($changeValue) {
$query->where('frozen_balance', '>=', abs($changeValue));
})
->increment('frozen_balance', $changeValue);
if ($changRow === 0) {
return false;
}
$newValue = User::query()->where('id', $userId)->value('frozen_balance') ?? '0.00';
//添加记录
$record = new UserBalanceRecord();
$record->user_id = $userId;
$record->type = $type;
$record->change_value = $changeValue;
$record->old_value = bcsub($newValue, (string)$changeValue, 2);
$record->new_value = $newValue;
$record->change_action = $isNaturalNumber ? UserBalanceRecord::CHANGE_TYPE_INC : UserBalanceRecord::CHANGE_TYPE_DEC;
$record->comment = $comment;
$record->status = UserBalanceRecord::STATUS_WAIT;
$record->targetable()->associate($model);
return $record->save();
} catch (\Exception $e) {
return false;
}
finally {
optional($lock)->release();
}
}
}
//CacheKey::LOCK 为一个常量,这里的定义的值是 “lock:”
具体使用
//记得传自己的参数哈
UserBalanceRecordService::changeBalance()
UserBalanceRecordService::changeFrozenBalance()
代码说明
- 读者可能会很好奇,代码里面怎么看不到启用事务代码的?这是因为
changeBalance与changeFrozenBalance方法的实现,是个相对底层的代码。调用这个方法的地方,往往还会有一些其他的数据库操作的,并且会在那里启用事务。所以这里不需要启用事务,这样子还可以减少事物嵌套了
边栏推荐
猜你喜欢

【HMS core】【Ads Kit】Huawei Advertising——Overseas applications are tested in China. Official advertisements cannot be displayed

三丁基-巯基膦烷「tBuBrettPhos Pd(allyl)」OTf),1798782-17-8

Shell之条件语句

打破传统电商格局,新型社交电商到底有什么优点?

移动流量的爆发式增长,社交电商如何选择商业模式

typescript40-class类的保护修饰符

接口测试如何准备测试数据

typescript41-class类的私有修饰符

typescript39-class类的可见修饰符

typescript42-readonly修饰符
随机推荐
接口测试框架实战(四)| 搞定 Schema 断言
Shell之条件语句
Coordinate knowledge in digital twin campus scenarios
刚上线就狂吸70W粉,新型商业模式“分享购”来了,你知道吗?
Common lipophilic cell membrane dyes DiO, Dil, DiR, Did spectrograms and experimental procedures
MCM box model modeling method and source analysis of atmospheric O3
typescript47-函数之间的类型兼容性
mysql 创建索引的三种方式
MySQL 入门:Case 语句很好用
9.新闻分类:多分类问题
Detailed explanation of MOSN reverse channel
Interface testing framework combat (3) | JSON request and response assertion
软件开发的最大的区别是什么?
数据库基本概述与SQL概述
超好用的画图工具推荐
typescript41-class类的私有修饰符
Unity2D horizontal board game tutorial 6 - enemy AI and attack animation
4.深度学习的几何解释与梯度的优化
【精讲】利用原生js实现todolist
3.张量运算