业务表解析-余额系统

业务表解析-余额系统

业务要求
  1. 有个地方可以查看用户的 可用余额 与 冻结余额
  2. 还有个地方可以查看用户余额(可用余额 + 冻结余额)变动的明细
  3. 后台可以查看用户余额变动明细,可通过类型,变更类型,甚至备注去匹配记录
业务例子
  1. 有那么一种关系,下级购物,上级可以获得佣金
  2. 当下级购物并且付款后,上级立马获得冻结余额。具体实现就是给上级创建一条增加冻结余额的记录,然后增加上级的冻结余额
  3. 当下级确认收货,并且订单得到结算后,就需要将冻结余额转移到可用余额中。具体实现就是给上级创建一条减少冻结余额的记录,然后上级减少冻结余额。接着,给上级创建一条添加可用余额的记录,然后增加上级的可用余额
表结构设计(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='用户余额流水表';

字段解释

  1. users.balance:可用余额,用来统计可以使用的余额,≥ 0
  2. users.frozen_balance:冻结余额,用来统计冻结的余额,≥ 0
  3. user_balance_records.status:入账状态,1=可用余额,2=冻结余额
  4. user_balance_records.targetable_type 与 user_balance_records.targetable_type_id, 对应引起余额变化的目标模型。例子:下单使用了余额,这里就是订单模型 与 订单ID了
  5. user_balance_records.type:类型,必须要足够精确。使用余额下单是一种类型,订单取消后,退还也是一种类型,使用余额发红包是一种,领取别人的红包是一种,因未领取完而回退的红包金额也是一种类型。这里,可能有人会问,既然已经有了目标模型,难道还无法确认什么类型吗?可以,但是不直观。例子:如果开发人员把充值与退款所消耗的余额都记录到了一条记录上(暂不考虑设计合理与否),此时仅由记录(targetable_type + targetable_type_id) 来识别的话,我们是无法一眼就看出这条记录是因为充值还是退款引起了,后期做搜索也不好做
  6. user_balance_records.change_action:值的变更类型,正数就是 增加 ,负数就是减少,冗余做筛选用的
  7. user_balance_records.change_value:变更值,负数时带符号
  8. user_balance_records.old_value:旧值,也就是变更前的 users.balance 或者 users.frozen_balance
  9. user_balance_records.new_value:新值,也就是变更后的 users.balance 或者 users.frozen_balance
  10. user_balance_records.comment:备注
  11. user_balance_records.finance_comment 财务备注,部分公司需要
程序思考
  1. 从表设计中可以发现,每一次余额的变动,都会修改 user_balance_records 与 users 表,所需存储记录时,必须加上事务
  2. 为了余额的准确性,必须先获得锁后,才能进行余额的相关操作

以此为界限,下面是 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()
代码说明
  1. 读者可能会很好奇,代码里面怎么看不到启用事务代码的?这是因为changeBalancechangeFrozenBalance方法的实现,是个相对底层的代码。调用这个方法的地方,往往还会有一些其他的数据库操作的,并且会在那里启用事务。所以这里不需要启用事务,这样子还可以减少事物嵌套了
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
客客威客系统KPPW是一款基于PHP MYSQL技术构架的威客系统 ,积客客团队多年实践和对威客模式商业化运作的大量调查分析而精心策划研发,是您轻松搭建威客网站的首选利器。KPPW针对威客 任务模型进行了细致的分析,提供完善威客任务流程控制解决方案,并将逐步分享威客系统专业化应用作为我们的发展目标。   客客威客系统KPPW更新说明: 修改 前台单页面文章图片不显示 oauth登陆问题 购买服务账号余额不足时选择在线支付进入支付宝支付出错 部分用户商品详细页无法打开 首页宽屏时成功案例第一个大图为服务时链接错误 用户中心基本设置联系方式处的msn,qq必填项改为非必填项 开启邮箱激活后,注册时邮箱激活不成功(成功了,其实就是显示问题) 发布商品页面第一步,协助流程中html标签不解析 admin/tpl/admin_tpl_edit_ad_one.htm页面代码有一小处有问题 发布商品第一步,提交时只验证了上传图片 发布任务上传的图片附件,图片点击放大后,左下角显示的不是文件名,而是“图片名称”几个字 个人店铺里面查看任务信息显示有问题,html标签未解析 中标动态价格没有做位数限制 发布商品上传图片无*,但是不上传图片进入不了下一步,解决办法加上* 计件任务交稿时间过期,无人交稿,没有正常退款 计件悬赏延期加价后,有剩余金额未返还雇主(解决方案:雇主选稿后就不能再延期加价) 增值工具后台价格设置为0元后,发布时未购买成功(解决方案:0元也要能购买成功) 忘记密码,输入用户名,提示不存在,问题GBK中文未转码 后台"咨询管理"-->“关于网站”-->"网站介绍"里面将添加文章功能删除 当用户余额不足,后台也没有开启在线支付接口。页面无提示。 发布服务上传图片没有做必须上传的验证. 后台"附件管理"里面删除附件无效,只能删除数据库数据,文件不能删除 后台文章分类前台没有生效 稿件列伪静态之后无法翻页 后台幻灯片广告位添加新广告后,新的广告没有排序功能了 协议交付页面当前用户退出,报404错误 速配任务,发布速配任务,账户余额不足,在用户中心速配任务列点付款,提示您已付款 豆瓣第三方登录图标 店铺html标签问题 登录注册验证码显示问题 返回顶部样式 未经过审核的商品,通过链接别人也能看到,且能购买 普通招标,后台配置的佣金设为0,前台发布普通招标,发布后是未付款的状态 忘记密码找回功能增加必须填写正确的邮箱 举报稿件时,后台屏蔽稿件,任务详细页还是稿件未被屏蔽 新增 更新皮肤 增加ie8的支持 增加ie7图标支持 后台管理目录自定义 主要功能: 全新UI风格设计 全新前后台UI风格设计,严格遵循W3C网页标准,采用HTML5、CSS3开发技术,KPPW2.0的整站设计充分考虑了威客行业用户体验需求和对未来拓展性开发的页面支持性。 商城交易模型化,拓展性更强 威客商城是以卖方市场为导向的新型威客模式,KPPW一直都引领着这一主题的创新发展。新版本在程序构架上让商品模块化,支持类似任务模型一样的独立开发。 全局代码构架改善,更安全更高效 新版本程序结构深入改造,面向对象MVC设计模式,模块化挂接。减少重复造轮子,增加了代码的重用性,需要的时可以安装这个模块,不需要时可以卸载这个模块。 用户经验权限体系改造更体贴 新版本重构了用户经验权限体系,针对威客应用的贴切需求,将雇主信用和威客能力进行了重新规划开发,可与其他更多的威客站点平移数据。 任务模型开发规范,流程更细致 针对威客任务交易日益增长的需求,KPPW对现有的悬赏任务、招标任务进行了重新的开发设计。在老版本基础上确定出了新的用户体验、任务权限、代码规范等多项标准,让任务模型自由拆解和开发拓展性更强。   KPPW2.0产品特色说明: 多年威客行业项目开发沉淀,历时半年开发,重写50W行代码,完全细节优化,客客团队新长征计划【KPPW2.0开发】即将完工。全新的威客应用解决方案,功能更加全面、体验更舒适,让您的威客网站更好运营。 一、全新UI风格设计,提升用户体验 全新前后台UI风格设计,严格遵循W3C网页标准,采用HTML5、CSS3开发技术,KPPW2.0的整站设计充分考虑了威客行业用户体验需求和对未来拓展性开发的页面支持性。从版本界面风格设计上,简洁明了主题突出;随时随地的工具栏,引导用户操作体验更加舒适;模板页面代码样式模块化,可针对局部自由开发完美兼容整站,且整站模板风格的切换和开发支持性也更加强; 二、全局代码构架改善,更安全更高效 新版本程序结构深入改造,面向对象MVC设计模式,模块化挂接。减少重复造轮子,增加了代码的重用性,需要的时可以安装这个模块,不需要时可以卸载这个模块。采用了数据缓存和模板缓存两种缓存机制,性能更佳。同时大量运用AJAX交互技术,使程序高效,快速的运行,让用户体验得到质的飞跃。新增mysql事务处理机制,解决高并发网站重要数据丢失,数据的不一至性。 三、任务模型开发规范,流程更细致 针对威客任务交易日益增长的需求,KPPW对现有的悬赏任务、招标任务进行了重新的开发设计。在老版本基础上确定出了新的用户体验、任务权限、代码规范等多项标准,让任务模型自由拆解和开发拓展性更强。另新版本中还新增了默认的任务模型,满足目前威客行业基础性任务模型运营的所有要求。 四、商城交易模型化,拓展性更强 威客商城是以卖方市场为导向的新型威客模式,KPPW一直都引领着这一主题的创新发展。新版本在程序构架上让商品模块化,支持类似任务模型一样的独立开发;且针对现有的服务和作品的业务功能进行了进一步细化的深入,满足了大部分的创意型产品的买卖交易。 五、用户经验权限体系改造更体贴 新版本重构了用户经验权限体系,针对威客应用的贴切需求,将雇主信用和威客能力进行了重新规划开发,可与其他更多的威客站点平移数据。在用户的操作权限方面,针对交易流程有独立的权限设置,同时整站还提供用户工具箱,可方便的进行拓展开发。不仅能够让用户体验更好,更为威客站长带来了更多的盈利手段。 六、模板标签数据调用更方便 优化了SEO,伪静态,网站能够更好的被各大搜索引擎收录,让访问量得到进一步的提高。模板标签调用使用简单快捷,支持站内,站外调用。重新开发的广告系统,针对全站的广告位进行了规划设计。除了可以上传广告之外,也可以调用其他广告代码,或者远程广告链接。 七、支付宝悬赏担保交易网站更具公信力 新版本新增最新推出的支付宝悬赏担保交易接口,开启后雇主和威客任务交易,资金冻结在用户自己的支付宝账户内。用户使用更加方便、公信力更强,特别适合威客新站的起步阶段。而系统在后台设置了接口开关,方便站长对这一策略的灵活调动,且不影响站内数据的显示。 八、中小站长互助任务信息,站点数据更饱和 为了满足广大中小站长,团队人手不足,新站建立维护难度大。KPPW后台站长可以开启站长互助任务信息数据,不仅能够饱和新站,而且能够得到有效宣传后的赏金。比其他新站录入不实任务数据更有意义。 九、多语言包、其他账号登陆细节亮点不断。。。 KPPW2.0对近一年多来站长们提交的各种需求细节进行了深入考虑,在整个系统中新增了不少细节功能,如威客地图、多语言包、其他账号登陆、短信通知、威客工具箱、广告系统改造等让网站功能变得丰富,也为站长盈利带来了更多机会。。。   前台首页界面演示图:   后台管理界面演示图:   相关阅读 同类推荐:站长常用源码

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值