HappinessWithdrawLog.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. <?php
  2. namespace addons\Happiness\common\models;
  3. use addons\Alitopay\common\enums\AlitoapyEnum;
  4. use addons\Alitopay\common\models\Order;
  5. use addons\Alitopay\common\services\AlipayService;
  6. use addons\Alitopay\common\services\SettingService;
  7. use addons\HuifuPay\common\models\HuifuPayOrderModel;
  8. use addons\HuifuPay\common\service\HuifuService;
  9. use addons\JoinPay\common\global_func\withdraw_way\WithdrawWay;
  10. use addons\Happiness\common\enums\HappinessEnum;
  11. use common\enums\AddonsEnum;
  12. use common\enums\PayTypeEnum;
  13. use common\enums\RedisKeyEnum;
  14. use common\enums\StatusEnum;
  15. use common\enums\UserWalletEnum;
  16. use addons\Happiness\common\enums\HappinessWithdrawWayEnum;
  17. use common\globals\GlobalInvoke;
  18. use common\helpers\LockHelper;
  19. use common\helpers\StringHelper;
  20. use common\models\common\PaymentTransfer;
  21. use common\models\user\User;
  22. use common\models\user\UserInfo;
  23. use Exception;
  24. use services\common\PayService;
  25. use services\common\UserWalletService;
  26. use yii\helpers\Json;
  27. use Yii;
  28. /**
  29. * This is the model class for table "qimall_addons_happiness_withdraw_log".
  30. *
  31. * @property int $id
  32. * @property int $mall_id 商城id
  33. * @property int $store_id 门店id
  34. * @property int $user_id 会员id
  35. * @property string $order_no 提现订单号
  36. * @property float $num 提现金额
  37. * @property float $actual_num 实际到账金额
  38. * @property float $poundage_num 手续费扣除金额
  39. * @property float $poundage_ratio 手续费比例
  40. * @property string $type 提现方式 bank--银行转账 balance--打款到余额
  41. * @property string $extra 额外信息
  42. * @property int $withdraw_status 提现状态 0--待打款 --已打款 2--驳回
  43. * @property int $status 状态[0禁用 1启用 -1删除]
  44. * @property string $remark 备注
  45. * @property int|null $created_at 添加时间戳
  46. * @property int|null $updated_at 更新时间戳
  47. * @property string|null $content 用户额外输入信息
  48. */
  49. class HappinessWithdrawLog extends \common\models\base\BaseActiveRecord
  50. {
  51. /**
  52. * {@inheritdoc}
  53. */
  54. public static function tableName()
  55. {
  56. return 'qimall_addons_happiness_withdraw_log';
  57. }
  58. /**
  59. * {@inheritdoc}
  60. */
  61. public function rules()
  62. {
  63. return [
  64. [['mall_id', 'store_id', 'user_id', 'withdraw_status', 'status', 'created_at', 'updated_at'], 'integer'],
  65. [['num', 'actual_num', 'poundage_num', 'poundage_ratio'], 'number'],
  66. [['extra', 'content'], 'string'],
  67. [['order_no', 'remark'], 'string', 'max' => 255],
  68. [['type'], 'string', 'max' => 45],
  69. ];
  70. }
  71. /**
  72. * {@inheritdoc}
  73. */
  74. public function attributeLabels()
  75. {
  76. return [
  77. 'id' => 'ID',
  78. 'mall_id' => 'Mall ID',
  79. 'store_id' => 'Store ID',
  80. 'user_id' => 'User ID',
  81. 'order_no' => 'Order No',
  82. 'num' => 'Num',
  83. 'actual_num' => 'Actual Num',
  84. 'poundage_num' => 'Poundage Num',
  85. 'poundage_ratio' => 'Poundage Ratio',
  86. 'type' => 'Type',
  87. 'extra' => 'Extra',
  88. 'withdraw_status' => 'Withdraw Status',
  89. 'status' => 'Status',
  90. 'remark' => 'Remark',
  91. 'created_at' => 'Created At',
  92. 'updated_at' => 'Updated At',
  93. 'content' => 'Content',
  94. ];
  95. }
  96. public function saveRecord($params)
  97. {
  98. if(!empty($params['type'])){
  99. switch ($params['type']){
  100. case "joinpay":
  101. case "bank":
  102. if(empty($params['bank_account'])||empty($params['bank_name'])||empty($params['name'])){
  103. return [
  104. 'state' => false,
  105. 'msg' => '请选择银行卡!'
  106. ];
  107. }
  108. break;
  109. case "wechat":
  110. if(empty($params['wechat_qrcode'])){
  111. return [
  112. 'state' => false,
  113. 'msg' => '请上传收款码!'
  114. ];
  115. }
  116. break;
  117. case "alipay_tax":
  118. if(empty($params['id_card'])){
  119. return [
  120. 'state' => false,
  121. 'msg' => '请填写支付宝身份证号码!'
  122. ];
  123. }
  124. if(empty($params['mobile'])){
  125. return [
  126. 'state' => false,
  127. 'msg' => '请填写支付宝账号!'
  128. ];
  129. }
  130. if(empty($params['name'])){
  131. return [
  132. 'state' => false,
  133. 'msg' => '请填写支付宝真实姓名!'
  134. ];
  135. }
  136. break;
  137. case "alipay_offline":
  138. case "alitopay":
  139. if(empty($params['name'])){
  140. return [
  141. 'state' => false,
  142. 'msg' => '请填写支付宝真实姓名!'
  143. ];
  144. }
  145. if(empty($params['mobile'])){
  146. return [
  147. 'state' => false,
  148. 'msg' => '请填写支付宝账号!'
  149. ];
  150. }
  151. break;
  152. case "huifu_balance":
  153. if(empty($params['huifu_no'])){
  154. return [
  155. 'state' => false,
  156. 'msg' => '请填写汇付号'
  157. ];
  158. }
  159. break;
  160. }
  161. }
  162. if ($params['price'] <= 0) {
  163. return [
  164. 'state' => false,
  165. 'msg' => '提现金额必须大于0!'
  166. ];
  167. }
  168. $exists = self::find()->where([
  169. 'mall_id' => $params['mall_id'], 'withdraw_status' => 0, 'user_id' => $params['user_id']
  170. ])->asArray()->one();// mycat不支持exists()用法
  171. if ($exists) {
  172. return [
  173. 'state' => false,
  174. 'msg' => '尚有未审核的提现申请!'
  175. ];
  176. }
  177. $store = HappinessStore::getOne([['id' => $params['store_id']]], false, [], false, 'id,mall_id,user_id,balance,total_expenses');
  178. //手续费设置
  179. $poundage_ratio = 0;
  180. //需要提现手续费金额
  181. $cash_amount = $params['price'];
  182. if ($params['type'] != 'balance') {
  183. $finance = \Yii::$app->services->setting->addons->get($params['mall_id'], HappinessEnum::ADDONS_NAME, 'basic');
  184. $poundage_ratio = $finance['balance_cash_service_fee'] ?? 0;
  185. }
  186. if ($store['balance'] < $params['price']) {
  187. return [
  188. 'state' => false,
  189. 'msg' => '提现金额超出可提现金额!'
  190. ];
  191. }
  192. if (!$params['type']) {
  193. return [
  194. 'state' => false,
  195. 'msg' => '请选择提现方式!'
  196. ];
  197. }
  198. if ($params['type'] == 'wechat') {
  199. if (!isset($params['wechat_qrcode'])) {
  200. return [
  201. 'state' => false,
  202. 'msg' => '请上传微信收款码!'
  203. ];
  204. }
  205. }
  206. if ($params['type'] == 'alipay') {
  207. if (!$params['mobile']) {
  208. return [
  209. 'state' => false,
  210. 'msg' => '请填写支付宝账号!'
  211. ];
  212. }
  213. if (!$params['name']) {
  214. return [
  215. 'state' => false,
  216. 'msg' => '请填写支付宝姓名!'
  217. ];
  218. }
  219. }
  220. if ($params['type'] == 'paid') {//代付银行卡
  221. if (!$params['name']) {
  222. return [
  223. 'state' => false,
  224. 'msg' => '请填写持卡人!'
  225. ];
  226. }
  227. if (!$params['bank_account']) {
  228. return [
  229. 'state' => false,
  230. 'msg' => '请填写银行卡号!'
  231. ];
  232. }
  233. }
  234. if ($params['type'] == 'alipay_tax') {
  235. if (!$params['mobile']) {
  236. return [
  237. 'state' => false,
  238. 'msg' => '请填写支付宝账号(手机号)'
  239. ];
  240. }
  241. if (!$params['name']) {
  242. return [
  243. 'state' => false,
  244. 'msg' => '请填写支付宝姓名'
  245. ];
  246. }
  247. if (!$params['id_card']) {
  248. return [
  249. 'state' => false,
  250. 'msg' => '请填写身份证信息'
  251. ];
  252. }
  253. $service = new \addons\AvoidTax\common\service\UserService(['mall_id' => $params['mall_id']]);
  254. if (!$service->idIsSignAndAddAuthOfUid(
  255. $params['id_card'], $params['name'], $params['user_id'], $params['mobile']
  256. )) {
  257. $res = $service->checkSign($params['user_id']);
  258. if ($res === false) {
  259. $res = $service->goToSing($params['user_id'], $params['mobile'], $params['id_card'], $params['name']);
  260. if (!$res) {
  261. return [
  262. 'state' => false,
  263. 'msg' => $service->error
  264. ];
  265. }
  266. // 跳转url签约
  267. return [
  268. 'state' => true,
  269. 'data' => $res
  270. ];
  271. }
  272. }
  273. }
  274. $cash = new HappinessWithdrawLog();
  275. $cash->mall_id = $params['mall_id'];
  276. $cash->user_id = $store['user_id'];
  277. $cash->store_id = $store['id'];
  278. $cash->num = $params['price'];
  279. $cash->actual_num = $params['price'];
  280. $cash->order_no = StringHelper::generateOrderNo('tx');
  281. $cash->status = StatusEnum::ENABLED;
  282. $cash->remark = $params['remark'] ?? '';
  283. $cash->content = Json::encode(['user_content' => $params['content']]);
  284. $cash->type = $params['type'];
  285. $cash->extra = Json::encode([
  286. 'name' => $params['name'],
  287. 'mobile' => $params['mobile'],
  288. 'bank_name' => $params['bank_name'],
  289. 'bank_account' => (string)$params['bank_account'],
  290. 'bank_account_type' => $params['bank_account_type'],
  291. 'wechat_qrcode' => $params['wechat_qrcode'],
  292. 'id_card' => $params['id_card'] ?? '',
  293. 'receive_bank_chanel_no' => $params['receive_bank_chanel_no'] ?? '',
  294. 'huifu_no' => $params['huifu_no'] ?? ''
  295. ], JSON_UNESCAPED_UNICODE);
  296. if ($poundage_ratio < 0 || $poundage_ratio > 100) {
  297. return [
  298. 'state' => false,
  299. 'msg' => '手续费设置不正确!'
  300. ];
  301. }
  302. //手续费
  303. $poundage = 0;
  304. if ($poundage_ratio > 0 && $poundage_ratio < 100) {
  305. //手续费:取小数点2位,截断
  306. $poundage = bcmul($poundage_ratio / 100, $cash_amount, 4);
  307. // 四舍五入,保留2位
  308. $poundage = number_format($poundage, 2, '.', '');
  309. $cash->actual_num = bcsub($params['price'], $poundage, 2);
  310. $cash->poundage_num = $poundage;
  311. $cash->poundage_ratio = $poundage_ratio;
  312. }
  313. if (intval(bcmul($cash->actual_num, 100)) < 1) {
  314. return [
  315. 'state' => false,
  316. 'msg' => '提现金额扣除费用后金额不足!'
  317. ];
  318. }
  319. $t = \Yii::$app->db->beginTransaction();
  320. try {
  321. switch ($params['type']) {
  322. case 'balance':
  323. //提现到个人余额
  324. $desc = '余额提现' . $cash->num;
  325. $data = [
  326. 'message_id' => 0, 'mall_id' => $store['mall_id'], 'user_id' => $store['user_id'],
  327. 'wallet_type' => 'balance',
  328. 'is_frozen' => false, 'unfrozen' => false, 'money' => abs($cash->actual_num),
  329. 'desc' => $desc, 'source_table' => 'user_wallet', 'source_table_id' => $cash->id,
  330. 'from_type' => UserWalletEnum::WITHDRAWAL,
  331. 'capital_type' => UserWalletEnum::RECHARGE_STORE_BALANCE,
  332. ];
  333. $wallet_service_status = UserWalletService::handle(0, $data);
  334. if (!$wallet_service_status) {
  335. throw new Exception('提现失败' . UserWalletService::getStaticError());
  336. }
  337. $cash->withdraw_status = 1;
  338. break;
  339. default:
  340. $cash->withdraw_status = 0;
  341. }
  342. if (!$cash->save()) {
  343. throw new Exception($cash->getErrorMessage());
  344. }
  345. $store->balance -= $params['price'];
  346. $store->total_expenses += $params['price'];
  347. $res = $store->save();
  348. if ($res == false) throw new Exception($store->getErrorMessage());
  349. $res_data = [
  350. 'state' => true,
  351. 'data' => ['cash_id' => $cash->id],
  352. 'msg' => ''
  353. ];
  354. $t->commit();
  355. } catch (Exception $e) {
  356. $t->rollBack();
  357. \Yii::error($e->getMessage());
  358. return [
  359. 'state' => false,
  360. 'msg' => '提现失败!' . $e->getMessage()
  361. ];
  362. }
  363. return $res_data;
  364. }
  365. public static function cashAudit(int $mall_id, array $params)
  366. {
  367. $log = self::findOne(['mall_id' => $mall_id, 'status' => StatusEnum::ENABLED, 'id' => $params['id'], 'withdraw_status' => 0]);
  368. if (empty($log)) {
  369. self::$static_error = '提现记录不存在';
  370. return false;
  371. }
  372. try {
  373. $res = false;
  374. switch ($params['withdraw_status']) {
  375. case 1:
  376. $cash = [
  377. 'actual_num' => $log['actual_num'],
  378. 'mall_id' => $log['mall_id'],
  379. 'user_id' => $log['user_id'],
  380. 'order_no' => $log['order_no'],
  381. 'id' => $log['id'],
  382. ];
  383. switch ($log['type']) {
  384. case "joinpay":
  385. //汇聚代付
  386. $extraArr = json_decode($log['extra'], true);
  387. $extra = json_encode([
  388. 'bank_account' => !empty($extraArr['bank_card_no'])?$extraArr['bank_card_no']:$extraArr['bank_account'],
  389. 'name' => !empty($extraArr['payer_name'])?$extraArr['payer_name']:$extraArr['name'],
  390. 'bank_account_type' =>$extraArr['bank_account_type'],
  391. ]);
  392. $cash['extra'] = $extra;
  393. $data['cash'] = json_decode(json_encode($cash));
  394. $data['title'] = '商户余额提现';
  395. $data['source'] = '3';
  396. $res = WithdrawWay::extensionWithdrawPay($data);
  397. break;
  398. case "alitopay":
  399. $extraArr = json_decode($log['extra'], true);
  400. $res = self::withdrawToPay($mall_id, $log['id'], $log['order_no'], $extraArr['name'], $extraArr['mobile'], $log['actual_num']);
  401. break;
  402. case "alipay_offline":
  403. case "bank":
  404. case "wechat":
  405. $res = true;
  406. break;
  407. case "auto":
  408. $userInfo = UserInfo::getOne([["user_id" => $log->user_id, "platform" => [User::PLATFORM_WECHAT, User::PLATFORM_MP_WX], "status" => StatusEnum::ENABLED]]);
  409. try {
  410. $cash['extra'] = [];
  411. $payment = new PayService();
  412. $paymentTransfer = new PaymentTransfer();
  413. $paymentTransfer->orderNo = PaymentTransfer::generate_order_no("TX");
  414. $paymentTransfer->user = $userInfo;
  415. $paymentTransfer->title = "收益提现";
  416. $paymentTransfer->amount = $log['actual_num'];
  417. $paymentTransfer->transferType = PayTypeEnum::WECHAT;
  418. $paymentTransfer->cashInfo = json_decode(json_encode($cash));
  419. $res = $payment->transfer($paymentTransfer);
  420. } catch (Exception $e) {
  421. throw new \Exception($e->getMessage());
  422. }
  423. break;
  424. case 'alipay_tax':
  425. break;
  426. case 'huifu_balance':
  427. //调用汇付余额支付
  428. $config = \Yii::$app->services->setting->addons->get($mall_id,
  429. AddonsEnum::ADDONS_NAME_HUIFU_PAY, 'basic');
  430. $extraArr = json_decode($log['extra'], true);
  431. $data = [
  432. 'goods_title' => '小店提现ID(' . $log['store_id'] . ')',
  433. 'amount' => $log['actual_num'],
  434. 'huifu_id' => $config['huifupay_huifu_id'],
  435. ];
  436. $item = [
  437. 'num' => $log['actual_num'],
  438. 'id' => $extraArr['huifu_no']
  439. ];
  440. $splitAccount[] = $item;
  441. $huifuService = new HuifuService($mall_id);
  442. $res = $huifuService->balancePay($data, $splitAccount);
  443. if ($res&&$res['trans_stat']!='S') {
  444. throw new \Exception($res['resp_desc']);
  445. }
  446. break;
  447. }
  448. if ($res) {
  449. $log->withdraw_status = $params['withdraw_status'];
  450. $log->remark = $params['remark'];
  451. $log->updated_at = time();
  452. $res = $log->save();
  453. if (!$res) {
  454. throw new \Exception($log->getErrorMessage());
  455. }
  456. }
  457. break;
  458. case 2:
  459. // 驳回提现申请,退回已经扣除的余额
  460. $lock_key = RedisKeyEnum::suffix(RedisKeyEnum::LOCK_STORE_BALANCE_WALLET, $log->store_id);
  461. $identification = uniqid();
  462. if (LockHelper::lock($lock_key, $identification, 100, 100)) {
  463. // 退回的金额
  464. $store = HappinessStore::findOne(['mall_id' => $mall_id, 'id' => $log->store_id, 'user_id' => $log->user_id]);
  465. $store->balance = bcadd($store->balance, $log->num, 2);
  466. $store->total_expenses = bcsub($store->total_expenses, $log->num, 2);
  467. $res_store = $store->save();
  468. $log->withdraw_status = $params['withdraw_status'];
  469. $log->remark = $params['remark'];
  470. $log->updated_at = time();
  471. $res = $log->save();
  472. LockHelper::uLock($lock_key, $identification);
  473. if (!$res_store) {
  474. throw new \Exception($store->getErrorMessage());
  475. }
  476. $res = true;
  477. } else {
  478. throw new \Exception('系统繁忙,请稍后再试');
  479. }
  480. break;
  481. }
  482. return $res;
  483. }catch (\Exception $e){
  484. self::$static_error = $e->getMessage();
  485. return false;
  486. }
  487. }
  488. public static function withdrawToPay($mall_id, $source_id, $order_no, $name, $mobile, $money)
  489. {
  490. // 获取设置
  491. $setting = (new SettingService($mall_id))->getSetting();
  492. if (empty($setting)) throw new \Exception('未进行代付配置');
  493. // 加锁
  494. $uniqid = uniqid();
  495. $lock_key = 'alitopay_order_lock_' . $mall_id;
  496. if (!LockHelper::lock($lock_key, $uniqid)) throw new \Exception('操作频繁请稍后重试');
  497. // 拼接数据入表
  498. $insert_data = [];
  499. $time = time();
  500. // 是否达到最低打款金额
  501. if ($money < AlitoapyEnum::MIX_WITHDRAW) {
  502. throw new \Exception("订单{$name},打款金额必须大于1元");
  503. }
  504. // 数据
  505. $data['mall_id'] = $mall_id;
  506. $data['order_sn'] = $time . rand(1000, 9999);
  507. $data['type'] = Order::TYPE_WITHDRAWAL;
  508. $data['money'] = $money;
  509. $data['name'] = $name;
  510. $data['account'] = $mobile;
  511. $data['source_id'] = $source_id;
  512. $data['source_sn'] = $order_no;
  513. $data['created_at'] = $data['updated_at'] = $time;
  514. $insert_data[] = $data;
  515. // 开启事务
  516. $db = Yii::$app->db->beginTransaction();
  517. try {
  518. // 批量插入
  519. Order::batchInsert($insert_data);
  520. // 批量修改打款中
  521. $db->commit();
  522. } catch (\Exception $e) {
  523. $db->rollBack();
  524. LockHelper::uLock($lock_key, $uniqid);
  525. throw new \Exception($e->getMessage());
  526. }
  527. // 批量进行打款
  528. $count = self::aliPay($mall_id, $insert_data);
  529. LockHelper::uLock($lock_key, $uniqid);
  530. return $count;
  531. }
  532. private static function aliPay($mall_id, array $list)
  533. {
  534. $aliapy = new AlipayService($mall_id);
  535. // 创建一个批次号
  536. $out_batch_no = 'TX' . date('Ymd') . $mall_id . rand(1000, 9999);
  537. $count = 0;
  538. $sub_msg = null;
  539. foreach ($list as $v) {
  540. // 打款
  541. $res = $aliapy->transfer(
  542. $v['money'], $v['name'], $v['account'], $v['order_sn']
  543. );
  544. if ($res['code'] == '10000') {
  545. // 打款成功
  546. Order::paySuccessBySourceIdAndOrderSn(
  547. $v['source_id'], $v['order_sn'],$res['order_id']
  548. );
  549. $count++;
  550. } else {
  551. // 打款失败
  552. Order::payFialBySourceIdAndOrderSn(
  553. $v['source_id'], $v['order_sn'], $res['sub_msg']
  554. );
  555. $sub_msg =$res['sub_msg'];
  556. }
  557. }
  558. // 设置批次号
  559. Order::setOutBatchNoByOrderSn($out_batch_no, array_column($list, 'order_sn'));
  560. if(isset($sub_msg)){
  561. throw new \Exception($sub_msg);
  562. }
  563. return $count;
  564. }
  565. public static function getWithdrawTypeMap($mall_id, $from = 'withdraw_balance', $discard = ['alipay_tax'])
  566. {
  567. $payment = \Yii::$app->services->setting->mall->get($mall_id, $from);
  568. if (empty($payment['balance_cash_type'])) return [];
  569. $cashType = $payment['balance_cash_type'];
  570. $cash_type = Json::decode($cashType);
  571. $default_withdraw_way = HappinessWithdrawWayEnum::getDefaultWithDrawWayMap(HappinessWithdrawWayEnum::WITHDRAW_TYPE_2);
  572. $extension_withdraw_way = GlobalInvoke::exec(GlobalInvoke::INVOKE_GET_WITHDRAW_EXTENSION_WAY, $mall_id, ['mall_id' => $mall_id]);
  573. $all_withdraw = array_merge($default_withdraw_way, $extension_withdraw_way);
  574. $type_map = [];
  575. if (!empty($cash_type)) {
  576. foreach ($all_withdraw as $key => $value) {
  577. if ($key == 'alipay_tax') {
  578. $type_map[$key] = $value['name'];
  579. } else {
  580. $type_map[$key] = $value;
  581. }
  582. }
  583. }
  584. return $type_map;
  585. }
  586. }