| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- <?php
- // +----------------------------------------------------------------------
- // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
- // +----------------------------------------------------------------------
- // | Copyright (c) 2016~2024 https://www.crmeb.com All rights reserved.
- // +----------------------------------------------------------------------
- // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
- // +----------------------------------------------------------------------
- // | Author: CRMEB Team <admin@crmeb.com>
- // +----------------------------------------------------------------------
- namespace app\common\repositories\user;
- use app\common\repositories\BaseRepository;
- use app\common\dao\user\UserExtractDao as dao;
- use app\common\repositories\wechat\WechatUserRepository;
- use crmeb\jobs\SendSmsJob;
- use crmeb\services\MiniProgramService;
- use crmeb\services\SwooleTaskService;
- use crmeb\services\WechatService;
- use FormBuilder\Factory\Elm;
- use think\exception\ValidateException;
- use think\facade\Db;
- use think\facade\Queue;
- use think\facade\Route;
- use think\facade\Log;
- /**
- * Class UserExtractRepository
- *
- * @mixin dao
- */
- class UserExtractRepository extends BaseRepository
- {
- /**
- * @var dao
- */
- protected $dao;
- //0 银行卡 1微信 2支付宝 3零钱 4 余额
- const EXTRACT_TYPE_BANK = 0;
- const EXTRACT_TYPE_WECHAT = 1;
- const EXTRACT_TYPE_ALIPAY = 2;
- const EXTRACT_TYPE_WEXIN = 3;
- const EXTRACT_TYPE_YUE = 4;
- /**
- * UserExtractRepository constructor.
- * @param dao $dao
- */
- public function __construct(dao $dao)
- {
- $this->dao = $dao;
- }
- /**
- * 检查给定ID的相关记录是否存在且状态为0。
- *
- * 此方法用于通过指定的ID和状态查询数据库中是否有相关记录。
- * 它封装了对数据库操作的调用,以便外部代码可以通过一个简单的调用,
- * 确定是否有必要进一步处理或显示数据。
- *
- * @param int $id 需要查询的记录的ID。
- * @return bool 如果找到至少一条状态为0的记录,则返回true;否则返回false。
- */
- public function getWhereCount($id)
- {
- // 定义查询条件,指定ID和状态
- $where['extract_id'] = $id;
- $where['status'] = 0;
- // 调用DAO层方法查询满足条件的记录数,并检查是否大于0
- return $this->dao->getWhereCount($where) > 0;
- }
- /**
- * 根据条件获取分页列表数据
- *
- * 本函数用于根据给定的条件数组 $where,从数据库中检索满足条件的数据列表。
- * 它支持分页查询,并返回当前页码的数据显示以及总数据条数。
- * 这样可以在前端实现数据的分页显示。
- *
- * @param array $where 查询条件数组,用于指定数据库查询的条件。
- * @param int $page 当前页码,用于指定要返回的数据页码。
- * @param int $limit 每页数据的数量,用于指定每页显示的数据条数。
- * @return array 返回包含 'count' 和 'list' 两个元素的数组,'count' 表示总数据条数,'list' 表示当前页的数据列表。
- */
- public function getList(array $where, $page, $limit)
- {
- // 初始化查询,根据 $where 条件搜索,并加载关联的用户信息,但只获取指定的用户字段
- $query = $this->dao->search($where)->with(['user' => function ($query) {
- // 关联查询用户信息,只获取 uid, avatar, nickname 三个字段
- $query->field('uid,avatar,nickname');
- }]);
- // 计算满足条件的总数据条数
- $count = $query->count();
- // 根据当前页码和每页数据数量,获取满足条件的数据列表
- $list = $query->page($page, $limit)->select();
- // 将总数据条数和当前页的数据列表一起返回
- return compact('count', 'list');
- }
- /**
- * 计算总提取价格
- * 该方法通过查询数据表中满足条件的记录,计算出所有记录的提取价格之和。
- * 主要用于统计已提取金额的总数,以便于财务结算或数据分析。
- *
- * @param array $where 查询条件
- * 默认情况下,查询条件包括状态为1的记录,表示有效或激活的状态。
- * 用户可以通过传递自定义条件来修改查询逻辑。
- * @return float 返回查询结果中所有记录的提取价格之和
- */
- public function getTotalExtractPrice($where = [])
- {
- // 将默认状态为1的条件与用户自定义条件合并,并执行查询
- // 查询结果为满足条件的所有记录的提取价格之和
- return $this->dao->search($where + ['status' => 1])->sum('extract_price');
- }
- /**
- * 计算用户提取总额
- *
- * 本函数用于查询指定用户的所有提取记录,并计算这些记录的提取金额总和。
- * 提取记录是通过搜索数据表中满足条件的条目来获取的,只包括状态为1(即有效)的记录。
- *
- * @param int $uid 用户ID
- * @return float 用户提取金额的总和
- */
- public function userTotalExtract($uid)
- {
- // 通过DAO层查询满足条件的提取记录,并计算提取金额总和
- return $this->dao->search(['status' => 1, 'uid' => $uid])->sum('extract_price');
- }
- /**
- * 提现申请创建函数
- * @param User $user 用户对象
- * @param array $data 提现数据
- * @return Extract 提现记录对象
- *
- * 该函数用于处理用户的提现申请,包括验证提现条件、更新用户佣金余额、
- * 创建提现记录等操作。在执行过程中,会触发相应的事件,以便于其他系统组件监听和处理相关逻辑。
- */
- public function create($user, $data)
- {
- // 触发提现前的提取事件,允许其他系统组件在这个阶段进行干预
- event('user.extract.before', compact('user', 'data'));
- $config = systemConfig(['extract_switch', 'sys_extension_type', 'withdraw_type', 'extract_type']);
- if (empty($config['withdraw_type'])) $config['withdraw_type'] = [self::EXTRACT_TYPE_YUE];
- $config['withdraw_type'][] = 3;
- if (!in_array($data['extract_type'], $config['withdraw_type']))
- throw new ValidateException('未开启该提现功能');
- //if ($data['extract_type'] == self::EXTRACT_TYPE_WEXIN && !$config['sys_extension_type'])
- // throw new ValidateException('未开启微信自动转账');
- if ($data['extract_type'] == self::EXTRACT_TYPE_WECHAT && $config['sys_extension_type'])
- $data['extract_type'] = self::EXTRACT_TYPE_WEXIN;
- //throw new ValidateException('仅支持微信自动转账');
- // 校验用户可提现金额是否满足最小值要求
- if ($user['brokerage_price'] < (systemConfig('user_extract_min')))
- throw new ValidateException('可提现金额不足');
- // 校验提现金额是否满足最小值要求
- if ($data['extract_price'] < (systemConfig('user_extract_min')))
- throw new ValidateException('提现金额不得小于最低额度');
- // 校验提现金额是否超过用户可提现余额
- if ($user['brokerage_price'] < $data['extract_price'])
- throw new ValidateException('提现金额不足');
- if ($data['extract_price'] >= 2000 && empty($data['real_name']) && $data['extract_type'] == self::EXTRACT_TYPE_WEXIN) {
- throw new ValidateException('提现金额大于等于2000时,收款人姓名必须填写');
- }
- $commission = systemConfig('extract_commission', 0);
- $data['commission'] = bcdiv(bcmul((string)$data['extract_price'], (string)$commission), '100', 2);
- $data['real_get'] = bcsub($data['extract_price'], $data['commission'], 2);
- if ($data['real_get'] <= 0) throw new ValidateException('未开启该提现功能');
- // 如果提现类型为微信,验证用户的微信OpenID是否存在
- if ($data['extract_type'] == self::EXTRACT_TYPE_WEXIN) {
- $make = app()->make(WechatUserRepository::class);
- $openid = $make->idByOpenId((int)$user['wechat_user_id']);
- if (!$openid) {
- $openid = $make->idByRoutineId((int)$user['wechat_user_id']);
- if (!$openid) throw new ValidateException('openID获取失败,请确认是微信用户');
- }
- }
- // 使用数据库事务来确保操作的原子性
- $userExtract = Db::transaction(function () use ($user, $data) {
- // 计算用户更新后的佣金余额
- $brokerage_price = bcsub($user['brokerage_price'], $data['extract_price'], 2);
- // 更新用户佣金余额
- $user->brokerage_price = $brokerage_price;
- $user->save();
- // 生成提现单号
- $data['extract_sn'] = $this->createSn();
- // 设置提现单的用户ID和余额
- $data['uid'] = $user['uid'];
- $data['balance'] = $brokerage_price;
- // 创建提现记录
- $res = $this->dao->create($data);
- if ($data['extract_type'] == self::EXTRACT_TYPE_YUE) {
- $this->switchStatus($res->extract_id, ['status' => 1]);
- }
- return $res;
- });
- if ($data['extract_type'] != self::EXTRACT_TYPE_YUE) {
- // 发送管理员通知,提醒有新的提现申请
- SwooleTaskService::admin('notice', [
- 'type' => 'extract',
- 'title' => '您有一条新的提醒申请',
- 'id' => $userExtract->extract_id
- ]);
- }
- // 触发提现完成事件,允许其他系统组件进行后续处理
- event('user.extract', compact('userExtract'));
- return $userExtract;
- }
- /**
- * 创建提现审核状态切换的表单
- *
- * 该方法用于生成一个包含审核状态切换选项的表单,特别是用于处理用户提现申请的审核状态。
- * 表单中包含一个单选按钮组,用于选择通过或拒绝提现申请,如果选择拒绝,还需要提供拒绝的原因。
- *
- * @param int $id 用户提现申请的ID,用于构建表单提交的URL,确保表单提交到正确的处理程序。
- * @return \Encore\Admin\Widgets\Form|\FormBuilder\Form
- */
- public function switchStatusForm($id)
- {
- // 构建表单URL,该URL将指向处理提现审核状态切换的控制器方法。
- $url = Route::buildUrl('systemUserExtractSwitchStatus', compact('id'))->build();
- // 创建表单对象,并设置表单标题。
- return Elm::createForm($url, [
- // 添加单选按钮组,用于选择提现申请的审核状态:通过或拒绝。
- Elm::radio('status', '审核状态:', 1)->options([['value' => -1, 'label' => '拒绝'], ['value' => 1, 'label' => '通过']])->control([
- // 当选择拒绝时,显示文本区域,用于输入拒绝的原因。
- ['value' => -1, 'rule' => [
- Elm::textarea('fail_msg', '拒绝原因:', '信息有误,请完善')->placeholder('请输入拒绝理由')->required()
- ]]
- ]),
- ])->setTitle('提现审核');
- }
- /**
- * 切换提取状态
- * 该方法用于处理提取记录的状态切换,涉及到资金操作和状态通知。
- *
- * @param int $id 提取记录ID
- * @param array $data 提交的数据,包含状态等信息
- * @throws ValidateException 如果用户不存在
- */
- public function switchStatus($id, $data)
- {
- // 根据提取ID获取提取记录
- $extract = $this->dao->getWhere(['extract_id' => $id]);
- // 根据提取记录中的用户ID获取用户信息
- $user = app()->make(UserRepository::class)->get($extract['uid']);
- // 如果用户不存在,抛出异常
- if (!$user) throw new ValidateException('用户不存在');
- // 获取系统配置的扩展类型
- $type = systemConfig('sys_extension_type');
- // 初始化返回数组和支付服务变量
- $ret = [];
- $service = null;
- $func = null;
- $brokerage = bcsub($user->brokerage_price, $extract['extract_price'], 2);
- // 初始化佣金价格变量
- $brokerage_price = 0;
- $out = [];
- //同意
- if ($data['status'] == 1) {
- $out = [
- 'type' => 'extract',
- 'data' => [
- 'link_id' => $id,
- 'status' => 1,
- 'title' => '佣金提现',
- 'number' => $extract['extract_price'],
- 'mark' => '成功佣金提现' . floatval($extract['extract_price']) . '元' . $extract['commission'] > 0 ? (',扣除手续费后实际到账' . $extract['real_get'] . '元') : '',
- 'balance' => $brokerage
- ]
- ];
- switch ($extract['extract_type']) {
- case self::EXTRACT_TYPE_WEXIN:
- if (in_array($type, [1, 2])) {
- // 根据扩展类型确定使用的支付方法
- $func = $type == 1 ? 'merchantPay' : 'companyPay';
- // 构建企业付款所需的信息
- $ret = [
- 'sn' => $extract['extract_sn'],
- 'price' => $extract['real_get'],
- 'mark' => '企业付款给用户:' . $user->nickname,
- 'batch_name' => '企业付款给用户:' . $user->nickname,
- 'realName' => $extract['real_name'] ?? ''
- ];
- // 尝试通过微信用户ID获取OpenID
- $openid = app()->make(WechatUserRepository::class)->idByOpenId((int)$user['wechat_user_id']);
- // 如果有OpenID,使用微信服务进行付款
- if ($openid) {
- $ret['openid'] = $openid;
- $service = WechatService::create();
- } else {
- // 如果没有OpenID,尝试获取小程序OpenID
- $routineOpenid = app()->make(WechatUserRepository::class)->idByRoutineId((int)$user['wechat_user_id']);
- // 如果没有小程序OpenID,抛出异常
- if (!$routineOpenid) throw new ValidateException('非微信用户不支持付款到零钱');
- $ret['openid'] = $routineOpenid;
- // 使用小程序服务进行付款
- $service = MiniProgramService::create();
- }
- }
- break;
- case self::EXTRACT_TYPE_YUE:
- $ret = ['extract' => $extract['real_get'], 'extract_id' => $id];
- $service = app()->make(UserExtractRepository::class);
- $func = 'toBalance';
- $out = [
- 'type' => 'now_money',
- 'data' => [
- 'link_id' => $id,
- 'status' => 1,
- 'title' => '佣金转入余额',
- 'number' => $extract['extract_price'],
- 'mark' => '成功转入余额' . floatval($extract['extract_price']) . '元' . $extract['commission'] > 0 ? (',扣除手续费后实际到账' . $extract['real_get'] . '元') : '',
- 'balance' => $brokerage
- ]
- ];
- break;
- default:
- break;
- }
- } else {
- // 如果数据中的状态为-1,计算新的佣金价格
- $brokerage_price = bcadd($user['brokerage_price'], $extract['extract_price'], 2);
- }
- $userBillRepository = app()->make(UserBillRepository::class);
- // 使用事务处理以下操作,确保数据的一致性
- Db::transaction(function () use ($id, $data, $user, $brokerage_price, $ret, $service, $func, $userBillRepository, $out) {
- // 触发状态切换前的事件
- event('user.extractStatus.before', compact('id', 'data'));
- // 如果有返回数组,调用相应的支付方法
- if ($data['status'] == 1 && $func) {
- $res = $service->{$func}($ret, $user);
- if ($res && $func == 'companyPay') {
- $this->dao->update(
- $id,
- [
- 'package_info' => $res['package_info'],
- 'transfer_bill_no' => $res['transfer_bill_no'],
- 'wechat_status' => $res['state'],
- 'wechat_app_id' => $res['app_id'],
- 'wechat_mch_id' => $res['mch_id']
- ]
- );
- };
- }
- // 如果有计算出的佣金价格,更新用户佣金信息
- if ($brokerage_price) {
- $user->brokerage_price = $brokerage_price;
- $user->save();
- }
- $data['check_time'] = time();
- // 更新提取记录状态
- $userExtract = $this->dao->update($id, $data);
- if ($out) $userBillRepository->decBill($user->uid, 'brokerage', $out['type'], $out['data']);
- // 触发状态切换后的事件
- event('user.extractStatus', compact('id', 'userExtract'));
- });
- // 推送发送短信的任务到队列
- Queue::push(SendSmsJob::class, ['tempId' => 'PAYMENT_RECEIVED', 'id' => $id]);
- }
- /**
- * 佣金提现到余额
- * @param $data
- * @param $user
- * @return void
- * @author Qinii
- */
- public function toBalance($data, $user)
- {
- $now_money = bcadd($user->now_money, $data['extract'], 2);
- $user->now_money = $now_money;
- $user->save();
- // 创建用户账单记录,佣金增加到余额
- app()->make(UserBillRepository::class)->incBill($user->uid, 'now_money', 'brokerage', [
- 'link_id' => 0,
- 'status' => 1,
- 'title' => '佣金转入余额',
- 'number' => $data['extract'],
- 'mark' => '成功转入余额' . floatval($data['extract']) . '元',
- 'balance' => $now_money
- ]);
- }
- /**
- * 创建一个唯一序列号
- *
- * 本函数旨在生成一个包含时间戳和随机数的唯一序列号,用于标识或唯一标记某个事物。
- * 序列号以"ue"开头,后面跟随毫秒级时间戳和一个随机数。这样可以确保在短时间内生成的序列号是唯一的。
- *
- * @return string 生成的唯一序列号
- */
- public function createSn()
- {
- // 获取当前时间的微秒和秒部分
- list($msec, $sec) = explode(' ', microtime());
- // 将微秒和秒转换为毫秒,并去掉小数点,确保序列号是整数
- $msectime = number_format((floatval($msec) + floatval($sec)) * 1000, 0, '', '');
- // 生成序列号:'ue' + 毫秒时间戳 + 随机数
- // 随机数范围确保在特定范围内,以避免生成的序列号在短时间内重复
- $sn = 'ue' . $msectime . mt_rand(10000, max(intval($msec * 10000) + 10000, 98369));
- return $sn;
- }
- /**
- * 获取用户的历史银行卡信息
- *
- * 本函数用于查询指定用户的历史银行卡记录。它通过用户的UID来检索数据,
- * 仅返回提取类型为0的记录,这通常表示用户的存款记录。返回的数据包括
- * 实名、银行代码、银行地址和银行名称,这些信息对于后续的银行相关操作
- * 或用户查询非常有用。
- *
- * @param int $uid 用户ID。这是查询用户历史银行卡记录的关键标识。
- * @return array 返回包含用户历史银行卡信息的数组。如果找不到相关信息,则返回空数组。
- */
- public function getHistoryBank($uid)
- {
- // 使用DAO对象进行查询,指定查询条件为UID和提取类型为0,按创建时间降序排序,并指定返回的字段。
- return $this->dao->getSearch(['uid' => $uid, 'extract_type' => 0])->order('create_time DESC')->field('real_name,bank_code,bank_address,bank_name')->find();
- }
- /**
- * 根据ID获取详细信息
- * 此方法通过ID从数据库中获取特定记录的详细信息,包括用户信息,使用懒加载模式来加载用户信息,
- * 仅当需要时才查询用户相关数据,以提高查询效率。
- *
- * @param int $id 主键ID,用于查询特定记录
- * @return array 返回查询到的详细信息数组
- * @throws ValidateException 如果未查询到任何信息,则抛出异常
- */
- public function detail(int $id)
- {
- // 使用懒加载方式获取数据,这里只查询基本信息,并通过回调函数加载用户详细信息
- $info = $this->dao->getWith($id, ['user' => function ($query) {
- // 精确查询用户信息,只获取必要的字段,以减少数据库查询负载
- $query->field('uid,avatar,nickname');
- }]);
- // 检查查询结果,如果为空,则抛出异常提示数据异常
- if (empty($info)) {
- throw new ValidateException('数据异常');
- }
- // 将查询结果转换为数组格式并返回
- return $info->toArray();
- }
- /**
- * 回调事件更新提现记录状态
- *
- * @param array $params
- * @return boolean
- */
- public function updateStatus(array $params): bool
- {
- $where = [];
- $where['transfer_bill_no'] = $params['data']['transfer_bill_no'];
- $where['extract_sn'] = $params['data']['out_bill_no'];
- $info = $this->dao->getWhere($where);
- if (!$info) {
- Log::info('商家提现记录变更:提现记录不存在。params:' . json_encode($params));
- return false;
- };
- $user = app()->make(UserRepository::class)->get($info['uid']);
- // 初始化佣金价格变量
- $brokerage_price = 0;
- $extractData = [];
- $extractData['wechat_status'] = $params['data']['state'];
- $extractData['status'] = 2; // 提现成功
- if ($extractData['wechat_status'] !== 'SUCCESS') {
- $extractData['status'] = -2; // 提现失败
- // 失败则回退佣金
- $brokerage_price = bcadd($user['brokerage_price'], $info['extract_price'], 2);
- }
- // 使用事务处理以下操作,确保数据的一致性
- Db::transaction(function () use ($info, $extractData, $user, $brokerage_price, $params) {
- // 如果有佣金,更新用户佣金信息
- if ($brokerage_price) {
- $user->brokerage_price = $brokerage_price;
- $user->save();
- }
- $res = $this->dao->update($info['extract_id'], $extractData);
- if (!$res) {
- Log::info('商家提现记录变更:提现记录状态变更失败。params:' . json_encode($params), ',res:' . json_encode($res));
- return false;
- };
- });
- Log::info('商家提现记录变更:提现记录状态变更成功,id:' . $info['extract_id'] . ',status:' . $params['data']['state']);
- return true;
- }
- }
|