UserExtractRepository.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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. $commission = systemConfig('extract_commission', 0);
  160. $data['commission'] = bcdiv(bcmul((string)$data['extract_price'], (string)$commission), '100', 2);
  161. $data['real_get'] = bcsub($data['extract_price'], $data['commission'], 2);
  162. if ($data['real_get'] <= 0) throw new ValidateException('未开启该提现功能');
  163. // 如果提现类型为微信,验证用户的微信OpenID是否存在
  164. if ($data['extract_type'] == self::EXTRACT_TYPE_WEXIN) {
  165. $make = app()->make(WechatUserRepository::class);
  166. $openid = $make->idByOpenId((int)$user['wechat_user_id']);
  167. if (!$openid) {
  168. $openid = $make->idByRoutineId((int)$user['wechat_user_id']);
  169. if (!$openid) throw new ValidateException('openID获取失败,请确认是微信用户');
  170. }
  171. }
  172. // 使用数据库事务来确保操作的原子性
  173. $userExtract = Db::transaction(function () use ($user, $data) {
  174. // 计算用户更新后的佣金余额
  175. $brokerage_price = bcsub($user['brokerage_price'], $data['extract_price'], 2);
  176. // 更新用户佣金余额
  177. $user->brokerage_price = $brokerage_price;
  178. $user->save();
  179. // 生成提现单号
  180. $data['extract_sn'] = $this->createSn();
  181. // 设置提现单的用户ID和余额
  182. $data['uid'] = $user['uid'];
  183. $data['balance'] = $brokerage_price;
  184. // 创建提现记录
  185. $res = $this->dao->create($data);
  186. if ($data['extract_type'] == self::EXTRACT_TYPE_YUE) {
  187. $this->switchStatus($res->extract_id, ['status' => 1]);
  188. }
  189. return $res;
  190. });
  191. if ($data['extract_type'] != self::EXTRACT_TYPE_YUE) {
  192. // 发送管理员通知,提醒有新的提现申请
  193. SwooleTaskService::admin('notice', [
  194. 'type' => 'extract',
  195. 'title' => '您有一条新的提醒申请',
  196. 'id' => $userExtract->extract_id
  197. ]);
  198. }
  199. // 触发提现完成事件,允许其他系统组件进行后续处理
  200. event('user.extract', compact('userExtract'));
  201. return $userExtract;
  202. }
  203. /**
  204. * 创建提现审核状态切换的表单
  205. *
  206. * 该方法用于生成一个包含审核状态切换选项的表单,特别是用于处理用户提现申请的审核状态。
  207. * 表单中包含一个单选按钮组,用于选择通过或拒绝提现申请,如果选择拒绝,还需要提供拒绝的原因。
  208. *
  209. * @param int $id 用户提现申请的ID,用于构建表单提交的URL,确保表单提交到正确的处理程序。
  210. * @return \Encore\Admin\Widgets\Form|\FormBuilder\Form
  211. */
  212. public function switchStatusForm($id)
  213. {
  214. // 构建表单URL,该URL将指向处理提现审核状态切换的控制器方法。
  215. $url = Route::buildUrl('systemUserExtractSwitchStatus', compact('id'))->build();
  216. // 创建表单对象,并设置表单标题。
  217. return Elm::createForm($url, [
  218. // 添加单选按钮组,用于选择提现申请的审核状态:通过或拒绝。
  219. Elm::radio('status', '审核状态:', 1)->options([['value' => -1, 'label' => '拒绝'], ['value' => 1, 'label' => '通过']])->control([
  220. // 当选择拒绝时,显示文本区域,用于输入拒绝的原因。
  221. ['value' => -1, 'rule' => [
  222. Elm::textarea('fail_msg', '拒绝原因:', '信息有误,请完善')->placeholder('请输入拒绝理由')->required()
  223. ]]
  224. ]),
  225. ])->setTitle('提现审核');
  226. }
  227. /**
  228. * 切换提取状态
  229. * 该方法用于处理提取记录的状态切换,涉及到资金操作和状态通知。
  230. *
  231. * @param int $id 提取记录ID
  232. * @param array $data 提交的数据,包含状态等信息
  233. * @throws ValidateException 如果用户不存在
  234. */
  235. public function switchStatus($id, $data)
  236. {
  237. // 根据提取ID获取提取记录
  238. $extract = $this->dao->getWhere(['extract_id' => $id]);
  239. // 根据提取记录中的用户ID获取用户信息
  240. $user = app()->make(UserRepository::class)->get($extract['uid']);
  241. // 如果用户不存在,抛出异常
  242. if (!$user) throw new ValidateException('用户不存在');
  243. // 获取系统配置的扩展类型
  244. $type = systemConfig('sys_extension_type');
  245. // 初始化返回数组和支付服务变量
  246. $ret = [];
  247. $service = null;
  248. $func = null;
  249. $brokerage = bcsub($user->brokerage_price, $extract['extract_price'], 2);
  250. // 初始化佣金价格变量
  251. $brokerage_price = 0;
  252. $out = [];
  253. //同意
  254. if ($data['status'] == 1) {
  255. $out = [
  256. 'type' => 'extract',
  257. 'data' => [
  258. 'link_id' => $id,
  259. 'status' => 1,
  260. 'title' => '佣金提现',
  261. 'number' => $extract['extract_price'],
  262. 'mark' => '成功佣金提现' . floatval($extract['extract_price']) . '元' . $extract['commission'] > 0 ? (',扣除手续费后实际到账' . $extract['real_get'] . '元') : '',
  263. 'balance' => $brokerage
  264. ]
  265. ];
  266. switch ($extract['extract_type']) {
  267. case self::EXTRACT_TYPE_WEXIN:
  268. if (in_array($type, [1, 2])) {
  269. // 根据扩展类型确定使用的支付方法
  270. $func = $type == 1 ? 'merchantPay' : 'companyPay';
  271. // 构建企业付款所需的信息
  272. $ret = [
  273. 'sn' => $extract['extract_sn'],
  274. 'price' => $extract['real_get'],
  275. 'mark' => '企业付款给用户:' . $user->nickname,
  276. 'batch_name' => '企业付款给用户:' . $user->nickname,
  277. 'realName' => $extract['real_name'] ?? ''
  278. ];
  279. // 尝试通过微信用户ID获取OpenID
  280. $openid = app()->make(WechatUserRepository::class)->idByOpenId((int)$user['wechat_user_id']);
  281. // 如果有OpenID,使用微信服务进行付款
  282. if ($openid) {
  283. $ret['openid'] = $openid;
  284. $service = WechatService::create();
  285. } else {
  286. // 如果没有OpenID,尝试获取小程序OpenID
  287. $routineOpenid = app()->make(WechatUserRepository::class)->idByRoutineId((int)$user['wechat_user_id']);
  288. // 如果没有小程序OpenID,抛出异常
  289. if (!$routineOpenid) throw new ValidateException('非微信用户不支持付款到零钱');
  290. $ret['openid'] = $routineOpenid;
  291. // 使用小程序服务进行付款
  292. $service = MiniProgramService::create();
  293. }
  294. }
  295. break;
  296. case self::EXTRACT_TYPE_YUE:
  297. $ret = ['extract' => $extract['real_get'], 'extract_id' => $id];
  298. $service = app()->make(UserExtractRepository::class);
  299. $func = 'toBalance';
  300. $out = [
  301. 'type' => 'now_money',
  302. 'data' => [
  303. 'link_id' => $id,
  304. 'status' => 1,
  305. 'title' => '佣金转入余额',
  306. 'number' => $extract['extract_price'],
  307. 'mark' => '成功转入余额' . floatval($extract['extract_price']) . '元' . $extract['commission'] > 0 ? (',扣除手续费后实际到账' . $extract['real_get'] . '元') : '',
  308. 'balance' => $brokerage
  309. ]
  310. ];
  311. break;
  312. default:
  313. break;
  314. }
  315. } else {
  316. // 如果数据中的状态为-1,计算新的佣金价格
  317. $brokerage_price = bcadd($user['brokerage_price'], $extract['extract_price'], 2);
  318. }
  319. $userBillRepository = app()->make(UserBillRepository::class);
  320. // 使用事务处理以下操作,确保数据的一致性
  321. Db::transaction(function () use ($id, $data, $user, $brokerage_price, $ret, $service, $func, $userBillRepository, $out) {
  322. // 触发状态切换前的事件
  323. event('user.extractStatus.before', compact('id', 'data'));
  324. // 如果有返回数组,调用相应的支付方法
  325. if ($data['status'] == 1 && $func) {
  326. $res = $service->{$func}($ret, $user);
  327. if ($res && $func == 'companyPay') {
  328. $this->dao->update(
  329. $id,
  330. [
  331. 'package_info' => $res['package_info'],
  332. 'transfer_bill_no' => $res['transfer_bill_no'],
  333. 'wechat_status' => $res['state'],
  334. 'wechat_app_id' => $res['app_id'],
  335. 'wechat_mch_id' => $res['mch_id']
  336. ]
  337. );
  338. };
  339. }
  340. // 如果有计算出的佣金价格,更新用户佣金信息
  341. if ($brokerage_price) {
  342. $user->brokerage_price = $brokerage_price;
  343. $user->save();
  344. }
  345. $data['check_time'] = time();
  346. // 更新提取记录状态
  347. $userExtract = $this->dao->update($id, $data);
  348. if ($out) $userBillRepository->decBill($user->uid, 'brokerage', $out['type'], $out['data']);
  349. // 触发状态切换后的事件
  350. event('user.extractStatus', compact('id', 'userExtract'));
  351. });
  352. // 推送发送短信的任务到队列
  353. Queue::push(SendSmsJob::class, ['tempId' => 'PAYMENT_RECEIVED', 'id' => $id]);
  354. }
  355. /**
  356. * 佣金提现到余额
  357. * @param $data
  358. * @param $user
  359. * @return void
  360. * @author Qinii
  361. */
  362. public function toBalance($data, $user)
  363. {
  364. $now_money = bcadd($user->now_money, $data['extract'], 2);
  365. $user->now_money = $now_money;
  366. $user->save();
  367. // 创建用户账单记录,佣金增加到余额
  368. app()->make(UserBillRepository::class)->incBill($user->uid, 'now_money', 'brokerage', [
  369. 'link_id' => 0,
  370. 'status' => 1,
  371. 'title' => '佣金转入余额',
  372. 'number' => $data['extract'],
  373. 'mark' => '成功转入余额' . floatval($data['extract']) . '元',
  374. 'balance' => $now_money
  375. ]);
  376. }
  377. /**
  378. * 创建一个唯一序列号
  379. *
  380. * 本函数旨在生成一个包含时间戳和随机数的唯一序列号,用于标识或唯一标记某个事物。
  381. * 序列号以"ue"开头,后面跟随毫秒级时间戳和一个随机数。这样可以确保在短时间内生成的序列号是唯一的。
  382. *
  383. * @return string 生成的唯一序列号
  384. */
  385. public function createSn()
  386. {
  387. // 获取当前时间的微秒和秒部分
  388. list($msec, $sec) = explode(' ', microtime());
  389. // 将微秒和秒转换为毫秒,并去掉小数点,确保序列号是整数
  390. $msectime = number_format((floatval($msec) + floatval($sec)) * 1000, 0, '', '');
  391. // 生成序列号:'ue' + 毫秒时间戳 + 随机数
  392. // 随机数范围确保在特定范围内,以避免生成的序列号在短时间内重复
  393. $sn = 'ue' . $msectime . mt_rand(10000, max(intval($msec * 10000) + 10000, 98369));
  394. return $sn;
  395. }
  396. /**
  397. * 获取用户的历史银行卡信息
  398. *
  399. * 本函数用于查询指定用户的历史银行卡记录。它通过用户的UID来检索数据,
  400. * 仅返回提取类型为0的记录,这通常表示用户的存款记录。返回的数据包括
  401. * 实名、银行代码、银行地址和银行名称,这些信息对于后续的银行相关操作
  402. * 或用户查询非常有用。
  403. *
  404. * @param int $uid 用户ID。这是查询用户历史银行卡记录的关键标识。
  405. * @return array 返回包含用户历史银行卡信息的数组。如果找不到相关信息,则返回空数组。
  406. */
  407. public function getHistoryBank($uid)
  408. {
  409. // 使用DAO对象进行查询,指定查询条件为UID和提取类型为0,按创建时间降序排序,并指定返回的字段。
  410. return $this->dao->getSearch(['uid' => $uid, 'extract_type' => 0])->order('create_time DESC')->field('real_name,bank_code,bank_address,bank_name')->find();
  411. }
  412. /**
  413. * 根据ID获取详细信息
  414. * 此方法通过ID从数据库中获取特定记录的详细信息,包括用户信息,使用懒加载模式来加载用户信息,
  415. * 仅当需要时才查询用户相关数据,以提高查询效率。
  416. *
  417. * @param int $id 主键ID,用于查询特定记录
  418. * @return array 返回查询到的详细信息数组
  419. * @throws ValidateException 如果未查询到任何信息,则抛出异常
  420. */
  421. public function detail(int $id)
  422. {
  423. // 使用懒加载方式获取数据,这里只查询基本信息,并通过回调函数加载用户详细信息
  424. $info = $this->dao->getWith($id, ['user' => function ($query) {
  425. // 精确查询用户信息,只获取必要的字段,以减少数据库查询负载
  426. $query->field('uid,avatar,nickname');
  427. }]);
  428. // 检查查询结果,如果为空,则抛出异常提示数据异常
  429. if (empty($info)) {
  430. throw new ValidateException('数据异常');
  431. }
  432. // 将查询结果转换为数组格式并返回
  433. return $info->toArray();
  434. }
  435. /**
  436. * 回调事件更新提现记录状态
  437. *
  438. * @param array $params
  439. * @return boolean
  440. */
  441. public function updateStatus(array $params): bool
  442. {
  443. $where = [];
  444. $where['transfer_bill_no'] = $params['data']['transfer_bill_no'];
  445. $where['extract_sn'] = $params['data']['out_bill_no'];
  446. $info = $this->dao->getWhere($where);
  447. if (!$info) {
  448. Log::info('商家提现记录变更:提现记录不存在。params:' . json_encode($params));
  449. return false;
  450. };
  451. $user = app()->make(UserRepository::class)->get($info['uid']);
  452. // 初始化佣金价格变量
  453. $brokerage_price = 0;
  454. $extractData = [];
  455. $extractData['wechat_status'] = $params['data']['state'];
  456. $extractData['status'] = 2; // 提现成功
  457. if ($extractData['wechat_status'] !== 'SUCCESS') {
  458. $extractData['status'] = -2; // 提现失败
  459. // 失败则回退佣金
  460. $brokerage_price = bcadd($user['brokerage_price'], $info['extract_price'], 2);
  461. }
  462. // 使用事务处理以下操作,确保数据的一致性
  463. Db::transaction(function () use ($info, $extractData, $user, $brokerage_price, $params) {
  464. // 如果有佣金,更新用户佣金信息
  465. if ($brokerage_price) {
  466. $user->brokerage_price = $brokerage_price;
  467. $user->save();
  468. }
  469. $res = $this->dao->update($info['extract_id'], $extractData);
  470. if (!$res) {
  471. Log::info('商家提现记录变更:提现记录状态变更失败。params:' . json_encode($params), ',res:' . json_encode($res));
  472. return false;
  473. };
  474. });
  475. Log::info('商家提现记录变更:提现记录状态变更成功,id:' . $info['extract_id'] . ',status:' . $params['data']['state']);
  476. return true;
  477. }
  478. }