UserExtractRepository.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2024 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace app\common\repositories\user;
  12. use app\common\repositories\BaseRepository;
  13. use app\common\dao\user\UserExtractDao as dao;
  14. use app\common\repositories\wechat\WechatUserRepository;
  15. use crmeb\jobs\SendSmsJob;
  16. use crmeb\services\MiniProgramService;
  17. use crmeb\services\SwooleTaskService;
  18. use crmeb\services\WechatService;
  19. use FormBuilder\Factory\Elm;
  20. use think\exception\ValidateException;
  21. use think\facade\Db;
  22. use think\facade\Queue;
  23. use think\facade\Route;
  24. use think\facade\Log;
  25. /**
  26. * Class UserExtractRepository
  27. *
  28. * @mixin dao
  29. */
  30. class UserExtractRepository extends BaseRepository
  31. {
  32. /**
  33. * @var dao
  34. */
  35. protected $dao;
  36. //0 银行卡 1微信 2支付宝 3零钱 4 余额
  37. const EXTRACT_TYPE_BANK = 0;
  38. const EXTRACT_TYPE_WECHAT = 1;
  39. const EXTRACT_TYPE_ALIPAY = 2;
  40. const EXTRACT_TYPE_WEXIN = 3;
  41. const EXTRACT_TYPE_YUE = 4;
  42. /**
  43. * UserExtractRepository constructor.
  44. * @param dao $dao
  45. */
  46. public function __construct(dao $dao)
  47. {
  48. $this->dao = $dao;
  49. }
  50. /**
  51. * 检查给定ID的相关记录是否存在且状态为0。
  52. *
  53. * 此方法用于通过指定的ID和状态查询数据库中是否有相关记录。
  54. * 它封装了对数据库操作的调用,以便外部代码可以通过一个简单的调用,
  55. * 确定是否有必要进一步处理或显示数据。
  56. *
  57. * @param int $id 需要查询的记录的ID。
  58. * @return bool 如果找到至少一条状态为0的记录,则返回true;否则返回false。
  59. */
  60. public function getWhereCount($id)
  61. {
  62. // 定义查询条件,指定ID和状态
  63. $where['extract_id'] = $id;
  64. $where['status'] = 0;
  65. // 调用DAO层方法查询满足条件的记录数,并检查是否大于0
  66. return $this->dao->getWhereCount($where) > 0;
  67. }
  68. /**
  69. * 根据条件获取分页列表数据
  70. *
  71. * 本函数用于根据给定的条件数组 $where,从数据库中检索满足条件的数据列表。
  72. * 它支持分页查询,并返回当前页码的数据显示以及总数据条数。
  73. * 这样可以在前端实现数据的分页显示。
  74. *
  75. * @param array $where 查询条件数组,用于指定数据库查询的条件。
  76. * @param int $page 当前页码,用于指定要返回的数据页码。
  77. * @param int $limit 每页数据的数量,用于指定每页显示的数据条数。
  78. * @return array 返回包含 'count' 和 'list' 两个元素的数组,'count' 表示总数据条数,'list' 表示当前页的数据列表。
  79. */
  80. public function getList(array $where, $page, $limit)
  81. {
  82. // 初始化查询,根据 $where 条件搜索,并加载关联的用户信息,但只获取指定的用户字段
  83. $query = $this->dao->search($where)->with(['user' => function ($query) {
  84. // 关联查询用户信息,只获取 uid, avatar, nickname 三个字段
  85. $query->field('uid,avatar,nickname');
  86. }]);
  87. // 计算满足条件的总数据条数
  88. $count = $query->count();
  89. // 根据当前页码和每页数据数量,获取满足条件的数据列表
  90. $list = $query->page($page, $limit)->select();
  91. // 将总数据条数和当前页的数据列表一起返回
  92. return compact('count', 'list');
  93. }
  94. /**
  95. * 计算总提取价格
  96. * 该方法通过查询数据表中满足条件的记录,计算出所有记录的提取价格之和。
  97. * 主要用于统计已提取金额的总数,以便于财务结算或数据分析。
  98. *
  99. * @param array $where 查询条件
  100. * 默认情况下,查询条件包括状态为1的记录,表示有效或激活的状态。
  101. * 用户可以通过传递自定义条件来修改查询逻辑。
  102. * @return float 返回查询结果中所有记录的提取价格之和
  103. */
  104. public function getTotalExtractPrice($where = [])
  105. {
  106. // 将默认状态为1的条件与用户自定义条件合并,并执行查询
  107. // 查询结果为满足条件的所有记录的提取价格之和
  108. return $this->dao->search($where + ['status' => 1])->sum('extract_price');
  109. }
  110. /**
  111. * 计算用户提取总额
  112. *
  113. * 本函数用于查询指定用户的所有提取记录,并计算这些记录的提取金额总和。
  114. * 提取记录是通过搜索数据表中满足条件的条目来获取的,只包括状态为1(即有效)的记录。
  115. *
  116. * @param int $uid 用户ID
  117. * @return float 用户提取金额的总和
  118. */
  119. public function userTotalExtract($uid)
  120. {
  121. // 通过DAO层查询满足条件的提取记录,并计算提取金额总和
  122. return $this->dao->search(['status' => 1, 'uid' => $uid])->sum('extract_price');
  123. }
  124. /**
  125. * 提现申请创建函数
  126. * @param User $user 用户对象
  127. * @param array $data 提现数据
  128. * @return Extract 提现记录对象
  129. *
  130. * 该函数用于处理用户的提现申请,包括验证提现条件、更新用户佣金余额、
  131. * 创建提现记录等操作。在执行过程中,会触发相应的事件,以便于其他系统组件监听和处理相关逻辑。
  132. */
  133. public function create($user,$data)
  134. {
  135. // 触发提现前的提取事件,允许其他系统组件在这个阶段进行干预
  136. event('user.extract.before',compact('user','data'));
  137. $config = systemConfig(['extract_switch','sys_extension_type','withdraw_type','extract_type']);
  138. if (empty($config['withdraw_type'])) $config['withdraw_type'] = [self::EXTRACT_TYPE_YUE];
  139. $config['withdraw_type'][] = 3;
  140. if (!in_array($data['extract_type'],$config['withdraw_type']))
  141. throw new ValidateException('未开启该提现功能');
  142. //if ($data['extract_type'] == self::EXTRACT_TYPE_WEXIN && !$config['sys_extension_type'])
  143. // throw new ValidateException('未开启微信自动转账');
  144. if ($data['extract_type'] == self::EXTRACT_TYPE_WECHAT && $config['sys_extension_type'])
  145. $data['extract_type'] = self::EXTRACT_TYPE_WEXIN;
  146. //throw new ValidateException('仅支持微信自动转账');
  147. // 校验用户可提现金额是否满足最小值要求
  148. if($user['brokerage_price'] < (systemConfig('user_extract_min')))
  149. throw new ValidateException('可提现金额不足');
  150. // 校验提现金额是否满足最小值要求
  151. if($data['extract_price'] < (systemConfig('user_extract_min')))
  152. throw new ValidateException('提现金额不得小于最低额度');
  153. // 校验提现金额是否超过用户可提现余额
  154. if($user['brokerage_price'] < $data['extract_price'])
  155. throw new ValidateException('提现金额不足');
  156. if($data['extract_price'] >= 2000 && empty($data['real_name']) && $data['extract_type'] == self::EXTRACT_TYPE_WEXIN) {
  157. throw new ValidateException('提现金额大于等于2000时,收款人姓名必须填写');
  158. }
  159. // 如果提现类型为微信,验证用户的微信OpenID是否存在
  160. if($data['extract_type'] == self::EXTRACT_TYPE_WEXIN) {
  161. $make = app()->make(WechatUserRepository::class);
  162. $openid = $make->idByOpenId((int)$user['wechat_user_id']);
  163. if (!$openid){
  164. $openid = $make->idByRoutineId((int)$user['wechat_user_id']);
  165. if(!$openid) throw new ValidateException('openID获取失败,请确认是微信用户');
  166. }
  167. }
  168. // 使用数据库事务来确保操作的原子性
  169. $userExtract = Db::transaction(function()use($user,$data){
  170. // 计算用户更新后的佣金余额
  171. $brokerage_price = bcsub($user['brokerage_price'],$data['extract_price'],2);
  172. // 更新用户佣金余额
  173. $user->brokerage_price = $brokerage_price;
  174. $user->save();
  175. // 生成提现单号
  176. $data['extract_sn'] = $this->createSn();
  177. // 设置提现单的用户ID和余额
  178. $data['uid'] = $user['uid'];
  179. $data['balance'] = $brokerage_price;
  180. // 创建提现记录
  181. $res = $this->dao->create($data);
  182. if ($data['extract_type'] == self::EXTRACT_TYPE_YUE) {
  183. $this->switchStatus($res->extract_id, ['status' => 1]);
  184. }
  185. return $res;
  186. });
  187. if ($data['extract_type'] != self::EXTRACT_TYPE_YUE) {
  188. // 发送管理员通知,提醒有新的提现申请
  189. SwooleTaskService::admin('notice', [
  190. 'type' => 'extract',
  191. 'title' => '您有一条新的提醒申请',
  192. 'id' => $userExtract->extract_id
  193. ]);
  194. }
  195. // 触发提现完成事件,允许其他系统组件进行后续处理
  196. event('user.extract',compact('userExtract'));
  197. return $userExtract;
  198. }
  199. /**
  200. * 创建提现审核状态切换的表单
  201. *
  202. * 该方法用于生成一个包含审核状态切换选项的表单,特别是用于处理用户提现申请的审核状态。
  203. * 表单中包含一个单选按钮组,用于选择通过或拒绝提现申请,如果选择拒绝,还需要提供拒绝的原因。
  204. *
  205. * @param int $id 用户提现申请的ID,用于构建表单提交的URL,确保表单提交到正确的处理程序。
  206. * @return \Encore\Admin\Widgets\Form|\FormBuilder\Form
  207. */
  208. public function switchStatusForm($id)
  209. {
  210. // 构建表单URL,该URL将指向处理提现审核状态切换的控制器方法。
  211. $url = Route::buildUrl('systemUserExtractSwitchStatus', compact('id'))->build();
  212. // 创建表单对象,并设置表单标题。
  213. return Elm::createForm($url, [
  214. // 添加单选按钮组,用于选择提现申请的审核状态:通过或拒绝。
  215. Elm::radio('status', '审核状态:', 1)->options([['value' => -1, 'label' => '拒绝'], ['value' => 1, 'label' => '通过']])->control([
  216. // 当选择拒绝时,显示文本区域,用于输入拒绝的原因。
  217. ['value' => -1, 'rule' => [
  218. Elm::textarea('fail_msg', '拒绝原因:', '信息有误,请完善')->placeholder('请输入拒绝理由')->required()
  219. ]]
  220. ]),
  221. ])->setTitle('提现审核');
  222. }
  223. /**
  224. * 切换提取状态
  225. * 该方法用于处理提取记录的状态切换,涉及到资金操作和状态通知。
  226. *
  227. * @param int $id 提取记录ID
  228. * @param array $data 提交的数据,包含状态等信息
  229. * @throws ValidateException 如果用户不存在
  230. */
  231. public function switchStatus($id,$data)
  232. {
  233. // 根据提取ID获取提取记录
  234. $extract = $this->dao->getWhere(['extract_id' => $id]);
  235. // 根据提取记录中的用户ID获取用户信息
  236. $user = app()->make(UserRepository::class)->get($extract['uid']);
  237. // 如果用户不存在,抛出异常
  238. if(!$user) throw new ValidateException('用户不存在');
  239. // 获取系统配置的扩展类型
  240. $type = systemConfig('sys_extension_type');
  241. // 初始化返回数组和支付服务变量
  242. $ret = [];
  243. $service = null;
  244. $func = null;
  245. $brokerage = bcsub($user->brokerage_price, $extract['extract_price'], 2);
  246. // 初始化佣金价格变量
  247. $brokerage_price = 0;
  248. $out = [];
  249. //同意
  250. if ($data['status'] == 1) {
  251. $out = [
  252. 'type' => 'extract',
  253. 'data' => [
  254. 'link_id' => $id,
  255. 'status' => 1,
  256. 'title' => '佣金提现',
  257. 'number' => $extract['extract_price'],
  258. 'mark' => '成功佣金提现' . floatval($extract['extract_price']) . '元',
  259. 'balance' => $brokerage
  260. ]
  261. ];
  262. switch ($extract['extract_type']) {
  263. case self::EXTRACT_TYPE_WEXIN:
  264. if (in_array($type,[1,2])) {
  265. // 根据扩展类型确定使用的支付方法
  266. $func = $type == 1 ? 'merchantPay' : 'companyPay';
  267. // 构建企业付款所需的信息
  268. $ret = [
  269. 'sn' => $extract['extract_sn'],
  270. 'price' => $extract['extract_price'],
  271. 'mark' => '企业付款给用户:'.$user->nickname,
  272. 'batch_name' => '企业付款给用户:'.$user->nickname,
  273. 'realName' => $extract['real_name'] ?? ''
  274. ];
  275. // 尝试通过微信用户ID获取OpenID
  276. $openid = app()->make(WechatUserRepository::class)->idByOpenId((int)$user['wechat_user_id']);
  277. // 如果有OpenID,使用微信服务进行付款
  278. if ($openid) {
  279. $ret['openid'] = $openid;
  280. $service = WechatService::create();
  281. } else {
  282. // 如果没有OpenID,尝试获取小程序OpenID
  283. $routineOpenid = app()->make(WechatUserRepository::class)->idByRoutineId((int)$user['wechat_user_id']);
  284. // 如果没有小程序OpenID,抛出异常
  285. if (!$routineOpenid) throw new ValidateException('非微信用户不支持付款到零钱');
  286. $ret['openid'] = $routineOpenid;
  287. // 使用小程序服务进行付款
  288. $service = MiniProgramService::create();
  289. }
  290. }
  291. break;
  292. case self::EXTRACT_TYPE_YUE:
  293. $ret = ['extract' => $extract['extract_price'], 'extract_id' => $id];
  294. $service = app()->make(UserExtractRepository::class);
  295. $func = 'toBalance';
  296. $out = [
  297. 'type' => 'now_money',
  298. 'data' => [
  299. 'link_id' => $id,
  300. 'status' => 1,
  301. 'title' => '佣金转入余额',
  302. 'number' => $extract['extract_price'],
  303. 'mark' => '成功转入余额' . floatval($extract['extract_price']) . '元',
  304. 'balance' => $brokerage
  305. ]
  306. ];
  307. break;
  308. default:
  309. break;
  310. }
  311. } else {
  312. // 如果数据中的状态为-1,计算新的佣金价格
  313. $brokerage_price = bcadd($user['brokerage_price'] ,$extract['extract_price'],2);
  314. }
  315. $userBillRepository = app()->make(UserBillRepository::class);
  316. // 使用事务处理以下操作,确保数据的一致性
  317. Db::transaction(function()use($id,$data,$user,$brokerage_price,$ret,$service,$func,$userBillRepository,$out){
  318. // 触发状态切换前的事件
  319. event('user.extractStatus.before',compact('id','data'));
  320. // 如果有返回数组,调用相应的支付方法
  321. if($data['status'] == 1 && $func) {
  322. $res = $service->{$func}($ret,$user);
  323. if ($res && $func == 'companyPay') {
  324. $this->dao->update(
  325. $id,
  326. [
  327. 'package_info' => $res['package_info'],
  328. 'transfer_bill_no' => $res['transfer_bill_no'],
  329. 'wechat_status' => $res['state'],
  330. 'wechat_app_id' => $res['app_id'],
  331. 'wechat_mch_id' => $res['mch_id']
  332. ]
  333. );
  334. };
  335. }
  336. // 如果有计算出的佣金价格,更新用户佣金信息
  337. if($brokerage_price){
  338. $user->brokerage_price = $brokerage_price;
  339. $user->save();
  340. }
  341. // 更新提取记录状态
  342. $userExtract = $this->dao->update($id,$data);
  343. if($out) $userBillRepository->decBill($user->uid, 'brokerage', $out['type'], $out['data']);
  344. // 触发状态切换后的事件
  345. event('user.extractStatus',compact('id','userExtract'));
  346. });
  347. // 推送发送短信的任务到队列
  348. Queue::push(SendSmsJob::class,['tempId' => 'PAYMENT_RECEIVED', 'id' =>$id]);
  349. }
  350. /**
  351. * 佣金提现到余额
  352. * @param $data
  353. * @param $user
  354. * @return void
  355. * @author Qinii
  356. */
  357. public function toBalance($data, $user)
  358. {
  359. $now_money = bcadd($user->now_money, $data['extract'], 2);
  360. $user->now_money = $now_money;
  361. $user->save();
  362. // 创建用户账单记录,佣金增加到余额
  363. app()->make(UserBillRepository::class)->incBill($user->uid, 'now_money', 'brokerage', [
  364. 'link_id' => 0,
  365. 'status' => 1,
  366. 'title' => '佣金转入余额',
  367. 'number' => $data['extract'],
  368. 'mark' => '成功转入余额' . floatval($data['extract']) . '元',
  369. 'balance' => $now_money
  370. ]);
  371. }
  372. /**
  373. * 创建一个唯一序列号
  374. *
  375. * 本函数旨在生成一个包含时间戳和随机数的唯一序列号,用于标识或唯一标记某个事物。
  376. * 序列号以"ue"开头,后面跟随毫秒级时间戳和一个随机数。这样可以确保在短时间内生成的序列号是唯一的。
  377. *
  378. * @return string 生成的唯一序列号
  379. */
  380. public function createSn()
  381. {
  382. // 获取当前时间的微秒和秒部分
  383. list($msec, $sec) = explode(' ', microtime());
  384. // 将微秒和秒转换为毫秒,并去掉小数点,确保序列号是整数
  385. $msectime = number_format((floatval($msec) + floatval($sec)) * 1000, 0, '', '');
  386. // 生成序列号:'ue' + 毫秒时间戳 + 随机数
  387. // 随机数范围确保在特定范围内,以避免生成的序列号在短时间内重复
  388. $sn = 'ue' . $msectime . mt_rand(10000, max(intval($msec * 10000) + 10000, 98369));
  389. return $sn;
  390. }
  391. /**
  392. * 获取用户的历史银行卡信息
  393. *
  394. * 本函数用于查询指定用户的历史银行卡记录。它通过用户的UID来检索数据,
  395. * 仅返回提取类型为0的记录,这通常表示用户的存款记录。返回的数据包括
  396. * 实名、银行代码、银行地址和银行名称,这些信息对于后续的银行相关操作
  397. * 或用户查询非常有用。
  398. *
  399. * @param int $uid 用户ID。这是查询用户历史银行卡记录的关键标识。
  400. * @return array 返回包含用户历史银行卡信息的数组。如果找不到相关信息,则返回空数组。
  401. */
  402. public function getHistoryBank($uid)
  403. {
  404. // 使用DAO对象进行查询,指定查询条件为UID和提取类型为0,按创建时间降序排序,并指定返回的字段。
  405. return $this->dao->getSearch(['uid' => $uid,'extract_type' => 0])->order('create_time DESC')->field('real_name,bank_code,bank_address,bank_name')->find();
  406. }
  407. /**
  408. * 根据ID获取详细信息
  409. * 此方法通过ID从数据库中获取特定记录的详细信息,包括用户信息,使用懒加载模式来加载用户信息,
  410. * 仅当需要时才查询用户相关数据,以提高查询效率。
  411. *
  412. * @param int $id 主键ID,用于查询特定记录
  413. * @return array 返回查询到的详细信息数组
  414. * @throws ValidateException 如果未查询到任何信息,则抛出异常
  415. */
  416. public function detail(int $id)
  417. {
  418. // 使用懒加载方式获取数据,这里只查询基本信息,并通过回调函数加载用户详细信息
  419. $info = $this->dao->getWith($id, ['user' => function ($query) {
  420. // 精确查询用户信息,只获取必要的字段,以减少数据库查询负载
  421. $query->field('uid,avatar,nickname');
  422. }]);
  423. // 检查查询结果,如果为空,则抛出异常提示数据异常
  424. if(empty($info)){
  425. throw new ValidateException('数据异常');
  426. }
  427. // 将查询结果转换为数组格式并返回
  428. return $info->toArray();
  429. }
  430. /**
  431. * 回调事件更新提现记录状态
  432. *
  433. * @param array $params
  434. * @return boolean
  435. */
  436. public function updateStatus(array $params) : bool
  437. {
  438. $where = [];
  439. $where['transfer_bill_no'] = $params['data']['transfer_bill_no'];
  440. $where['extract_sn'] = $params['data']['out_bill_no'];
  441. $info = $this->dao->getWhere($where);
  442. if(!$info) {
  443. Log::info('商家提现记录变更:提现记录不存在。params:'.json_encode($params));
  444. return false;
  445. };
  446. $user = app()->make(UserRepository::class)->get($info['uid']);
  447. // 初始化佣金价格变量
  448. $brokerage_price = 0;
  449. $extractData = [];
  450. $extractData['wechat_status'] = $params['data']['state'];
  451. $extractData['status'] = 2; // 提现成功
  452. if($extractData['wechat_status'] !== 'SUCCESS') {
  453. $extractData['status'] = -2; // 提现失败
  454. // 失败则回退佣金
  455. $brokerage_price = bcadd($user['brokerage_price'] ,$info['extract_price'],2);
  456. }
  457. // 使用事务处理以下操作,确保数据的一致性
  458. Db::transaction(function()use($info, $extractData, $user, $brokerage_price, $params){
  459. // 如果有佣金,更新用户佣金信息
  460. if($brokerage_price){
  461. $user->brokerage_price = $brokerage_price;
  462. $user->save();
  463. }
  464. $res = $this->dao->update($info['extract_id'], $extractData);
  465. if(!$res) {
  466. Log::info('商家提现记录变更:提现记录状态变更失败。params:'.json_encode($params),',res:'.json_encode($res));
  467. return false;
  468. };
  469. });
  470. Log::info('商家提现记录变更:提现记录状态变更成功,id:'.$info['extract_id'].',status:'.$params['data']['state']);
  471. return true;
  472. }
  473. }