当前位置:网站首页>Business table analysis - balance system
Business table analysis - balance system
2022-08-03 05:15:00 【DBCai】
Business table analysis-余额系统
业务要求
- There is a place to view the user's 可用余额 与 冻结余额
- There is also a place to check user balances(可用余额 + 冻结余额)Details of changes
- You can view the details of user balance changes in the background,可通过类型,变更类型,Even notes to match records
业务例子
- There is such a relationship,Lower level shopping,Superiors can get commissions
- After shopping at the next level and paying,The superior immediately obtained the frozen balance.The specific implementation is to create a record to increase the frozen balance for the superior,Then increase the frozen balance of the superior
- Confirm receipt of goods at the next level,And after the order is settled,It is necessary to transfer the frozen balance to the available balance.The specific implementation is to create a record for the superior to reduce the frozen balance,The superior then reduces the frozen balance.接着,Create a record for the superior to add the available balance,Then increase the available balance of the superior
表结构设计(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;
//Balance sheet
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='User balance flow meter';
字段解释
- users.balance:可用余额,Used to count the available balance,≥ 0
- users.frozen_balance:冻结余额,Used to count the frozen balance,≥ 0
- user_balance_records.status:入账状态,1=可用余额,2=冻结余额
- user_balance_records.targetable_type 与 user_balance_records.targetable_type_id, Corresponds to the target model that causes the balance to change.例子:The balance was used to place an order,Here is the order model 与 订单ID了
- user_balance_records.type:类型,Must be precise enough.Placing an order with a balance is one type,订单取消后,Refunds are also a type,Using the balance to send red packets is one kind,Receiving other people's red envelopes is one of them,The amount of red envelopes refunded due to incomplete collection is also a type.这里,可能有人会问,Now that you have the target model,Can't confirm what type?可以,但是不直观.例子:If the developer records the balance consumed by the recharge and refund in a record(Whether the design is reasonable or not is not considered for the time being),At this time only by the record(targetable_type + targetable_type_id) to identify,We can't tell at a glance whether this record is caused by a recharge or a refund,It's not easy to do a search later
- user_balance_records.change_action:The type of change for the value,正数就是 增加 ,A negative number is a decrease,Redundancy is used for filtering
- user_balance_records.change_value:变更值,Negative numbers are signed
- 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 财务备注,Some companies need it
Program thinking
- It can be found from the table design,every change in balance,都会修改 user_balance_records 与 users 表,when required to store records,Transactions must be added
- for the accuracy of the balance,The lock must be acquired first,To perform operations related to the balance
以此为界限,下面是 laravel 中的具体实现
数据表迁移文件
//Add a balance field to the user table up 方法(originally existedusers 表,None insideID)
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('冻结余额');
});
}
//Create balance records 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('User balance flow meter');
});
}
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 old amount * @property string $new_value new amount * @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 => '冻结余额'
];
/** * 类型:Change according to the type of balance,自行添加 */
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 {
// Wait the most to acquire a lock 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();
}
}
/** * Modify the frozen balance * * @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 {
// Wait the most to acquire a lock 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 为一个常量,The defined value here is “lock:”
具体使用
//Remember to pass your own parameters
UserBalanceRecordService::changeBalance()
UserBalanceRecordService::changeFrozenBalance()
代码说明
- Readers may be curious,How can I not see the enable transaction code in the code?这是因为
changeBalance
与changeFrozenBalance
方法的实现,Is a relatively low-level code.调用这个方法的地方,There are often some other database operations,and transactions will be enabled there.So there is no need to enable transactions here,This can also reduce the nesting of things
边栏推荐
- 用户密码加密工具
- 【HMS core】【Ads Kit】华为广告——海外应用在国内测试正式广告无法展示
- Harmony OS Date ano UI 】 【 】 the basic operation
- User password verification
- IO流及其操作
- Shell条件语句判断
- Shell conditional statement judgment
- 1089 狼人杀-简单版 (20 分)
- 1079 延迟的回文数 (20 分)
- Modified BiotinDIAZO-Biotin-PEG3-DBCO|diazo-biotin-tripolyethylene glycol-diphenylcyclooctyne
猜你喜欢
typescript47-函数之间的类型兼容性
idea使用@Autowired注解爆红原因及解决方法
FileZilla 搭建ftp服务器
Tag stack - stack monotonically preparatory knowledge - lt. 739. The daily temperature
常见亲脂性细胞膜染料DiO, Dil, DiR, Did光谱图和实验操作流程
Build your own web page on the Raspberry Pi (2)
阿里云对象存储oss私有桶生成链接
DFS's complement to pruning
Online password generator tool recommendation
Exception(异常) 和 Error(错误)区别解析
随机推荐
背压机制
Interface testing framework of actual combat (2) | interface request assertion
【Harmony OS】【ARK UI】轻量级数据存储
Shell conditional statement judgment
1069 微博转发抽奖 (20 分)(C语言)
Modified BiotinDIAZO-Biotin-PEG3-DBCO|diazo-biotin-tripolyethylene glycol-diphenylcyclooctyne
【Harmony OS】【ARK UI】Date 基本操作
idea uses @Autowired annotation to explain the reasons and solutions
[Fine talk] Using native js to implement todolist
Odps temporary query can write SQL, turned out to a named?
IO流及其操作
C# async and multithreading
JDBC与连接池
Shell条件语句判断
1060 爱丁顿数 (25 分)
js中的闭包
Exception (abnormal) and Error (error) difference analysis
超好用的画图工具推荐
【精讲】利用原生js实现todolist
2022暑假牛客多校联赛第一场