当前位置:网站首页>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
边栏推荐
- 接口测试实战| GET/POST 请求区别详解
- DFS对剪枝的补充
- 用户密码验证
- 建立树形结构
- 获取Ip工具类
- Interface test Mock combat (2) | Combined with jq to complete batch manual Mock
- 接口测试框架实战(三)| JSON 请求与响应断言
- [Fine talk] Using native js to implement todolist
- Interface Test Framework Practice (4) | Get Schema Assertion
- 【HMS core】【Ads Kit】华为广告——海外应用在国内测试正式广告无法展示
猜你喜欢

Modified BiotinDIAZO-Biotin-PEG3-DBCO|diazo-biotin-tripolyethylene glycol-diphenylcyclooctyne

js实现一个 bind 函数

Bubble sort in c language structure

2. 两数相加

Interface Test Framework Practice | Process Encapsulation and Test Case Design Based on Encrypted Interface

Jmeter 模拟多用户登录的两种方法

junit总结

Windows 安装PostgreSQL

Power button 561. An array of split

IO process thread -> thread -> day5
随机推荐
Talking about GIS Data (6) - Projected Coordinate System
1094 谷歌的招聘 (20 分)
How to use the interface management tool YApi?Beautiful, easy to manage, super easy to use
获取Ip工具类
Interface testing framework combat (3) | JSON request and response assertion
High availability, two locations and three centers
typescript39-class类的可见修饰符
技术分享 | 接口自动化测试中如何对xml 格式做断言验证?
Kotlin-Flow common encapsulation class: the use of StateFlow
closures in js
Tributyl-mercaptophosphane "tBuBrettPhos Pd(allyl)" OTf), 1798782-17-8
FileZilla 搭建ftp服务器
背压机制
反射注解基础
2022暑假牛客多校联赛第一场
4.如何避免缓存穿透、缓存击穿、缓存雪崩
Secondary development of WinForm controls
Concepts and Methods of Exploratory Testing
Common fluorescent dyes to modify a variety of groups and its excitation and emission wavelength data in the data
User password encryption tool