Browse Source

支持微信付款到零钱

joe 4 năm trước cách đây
mục cha
commit
0da7029fe0

+ 13 - 5
app/admin/controller/Test.php

@@ -12,6 +12,7 @@ use app\models\redis\SystemCarousel;
 use app\models\system\SystemPool;
 use app\models\user\UserSearch;
 use app\models\user\WechatUser;
+use crmeb\payment\MachantPay;
 use crmeb\services\async\LuckyExtACalc;
 use crmeb\services\async\LuckyExtBCalc;
 use crmeb\services\async\task\AsyncClass;
@@ -99,18 +100,25 @@ class Test
     {
         $ur = new UserRds();
         $ur->set(1, 'streak', 3);
-        echo $ur->get(1, 'streak');
-        echo $ur->get(1, 'notexists');
+        echo $ur->get(1, 'streak') . '</br>';
+        echo $ur->get(1, 'notexists') . '</br>';
 
         $ur->sets(1, ['name'=>'didi', 'age'=>28]);
-        print_r($ur->gets(1, ['name', 'age']));
-        print_r($ur->gets(1, ['friends', 'teacher']));
+        print_r($ur->gets(1, ['name', 'age'])) . '</br>';
+        print_r($ur->gets(1, ['friends', 'teacher'])) . '</br>';
 
-        print_r($ur->getAll(1));
+        print_r($ur->getAll(1)) . '</br>';
+    }
+
+    protected function test_enterprise_pay()
+    {
+        list($ok, $ec, $es) = MachantPay::toWeixin('oFWXD4lYL1n9jupjlWx7Qt6Qdu7Y', 'wxmp00000001', 100, 'test', 'yyk');
+        var_dump($ok, $ec, $es);
     }
 
     public function test()
     {
+        // $this->test_enterprise_pay();
         // echo $this->test_async_func();
         // $this->test_redis();
         // echo UserSearch::InsertHistory(1, "good");

+ 50 - 43
app/admin/controller/finance/UserExtract.php

@@ -13,6 +13,8 @@ use think\facade\Route as Url;
 use crmeb\services\JsonService;
 use app\admin\model\user\UserExtract as UserExtractModel;
 use crmeb\services\{UtilService as Util, FormBuilder as Form};
+use crmeb\services\payment\PaymentService;
+use app\admin\model\user\UserExtract as UserExtractAdmin;
 
 /**
  * 用户提现管理
@@ -127,66 +129,71 @@ class UserExtract extends AuthController
     }
 
     /**
-     * 拒绝提现操作
+     * @api {post} /fail 拒绝提现
+     * @apiName ExtractFail
+     * @apiGroup Finance
+     * 
+     * @apiParam {int} id: user_extract id
+     * @apiBody {string} fail_msg fail reason
+     * 
+     * @apiSuccessExample:
+     * {
+     *  "status": 200,
+     *  "msg": "操作成功"
+     * }
+     * @apiErrorExample:
+     * {
+     *  "status": 400,
+     *  "msg": "error reason"
+     * }
      */
     public function fail($id)
     {
-        if (!UserExtractModel::be(['id' => $id, 'status' => EXTRACT_AUDITING])) {
-            return JsonService::fail('操作记录不存在或状态错误!');
-        }
         $fail_msg = request()->post();
-        $extract = UserExtractModel::get($id);
-        if (!$extract) {
-            return JsonService::fail('操作记录不存在!');
-        }
-        if ($extract->status == EXTRACT_SUC) {
-            return JsonService::fail('已经提现,错误操作');
-        }
-        if ($extract->status == EXTRACT_FAILED) {
-            return JsonService::fail('您的提现申请已被拒绝,请勿重复操作!');
-        }
-        UserExtractModel::beginTrans();
-        $res = UserExtractModel::changeFail($id, $fail_msg['message']);
-        if ($res) {
-            UserExtractModel::rollbackTrans();
-            event('UserExtractFail', [$extract, $fail_msg]);
+        list($suc, $msg) = PaymentService::user_extract_reject($id, $fail_msg['message']);
+        if ($suc) {
             return JsonService::successful('操作成功!');
         } else {
-            UserExtractModel::commitTrans();
-            return JsonService::fail('操作失败!');
+            return JsonService::fail($msg);
         }
     }
 
     /**
-     * 提现通过操作
-     * @param int $id: user_extract 表 id
+     * @api {post} /succ 提现通过 (后台操作)
+     * @apiName ExtractSucc
+     * @apiGroup Finance
+     * 
+     * @apiParam {int} id user_extract id
+     * 
+     * @apiSuccessExample:
+     * {
+     *  "status": 200,
+     *  "msg": "操作成功"
+     * }
+     * @apiErrorExample:
+     * {
+     *  "status": 400,
+     *  "msg": "error reason"
+     * }
      */
     public function succ($id)
     {
-        if (!UserExtractModel::be(['id' => $id, 'status' => EXTRACT_AUDITING])) {
-            return JsonService::fail('操作记录不存在或状态错误!');
-        }
-            
-        $extract = UserExtractModel::get($id);
-        if (!$extract) {
-            return JsonService::fail('操作记录不存!');
+        $extractInfo = UserExtractAdmin::get($id);
+        if (!$extractInfo) {
+            return JsonService::fail('记录不存在');
         }
-        if ($extract->status == EXTRACT_SUC) {
-            return JsonService::fail('您已提现,请勿重复提现!');
-        }
-        if ($extract->status == EXTRACT_FAILED) {
-            return JsonService::fail('您的提现申请已被拒绝!');
+        // API 转账
+        list($suc, $ec, $es) = PaymentService::extract_by_api($extractInfo);
+        if (!$suc) {
+            return JsonService::fail($es);
         }
 
-        UserExtractModel::beginTrans();
-        $res = UserExtractModel::changeSuccess($id);
-        if ($res) {
-            UserExtractModel::commitTrans();
-            event('UserExtractSucc', [$extract]);
-            return JsonService::successful('操作成功!');
+        list($suc, $msg) = PaymentService::user_extract_passed($extractInfo);
+        if (!$suc) {
+            return JsonService::fail($msg);
         } else {
-            UserExtractModel::rollbackTrans();
-            return JsonService::fail('操作失败!');
+            return JsonService::successful('操作成功');
         }
+
     }
 }

+ 61 - 55
app/admin/model/user/UserExtract.php

@@ -85,6 +85,11 @@ class UserExtract extends BaseModel
         return self::page($model, $where);
     }
 
+    /**
+     * 其他函數調用本函數需要放到事務處理的作用域內。
+     * 
+     * 本函數內部不使用事務是因爲若外層調用方使用事物,會事務嵌套。也可能需要和其他邏輯組成一個大的事物
+     */
     public static function changeFail($id, $fail_msg)
     {
         $fail_time = time();
@@ -94,66 +99,67 @@ class UserExtract extends BaseModel
         $uid = $data['uid'];
         $status = EXTRACT_FAILED;
         $User = User::where('uid', $uid)->find()->toArray();
-        UserBill::income('提现失败', $uid, 'now_money', 'extract', $extract_number, $id, bcadd($User['now_money'], $extract_number, 2), $mark);
-        User::bcInc($uid, 'brokerage_price', $extract_number, 'uid');
-        $extract_type = '未知方式';
-        switch ($data['extract_type']) {
-            case 'alipay':
-                $extract_type = '支付宝';
-                break;
-            case 'bank':
-                $extract_type = '银行卡';
-                break;
-            case 'weixin':
-                $extract_type = '微信';
-                break;
-        }
-        if (strtolower($User['user_type']) == 'wechat') {
-            WechatTemplateService::sendTemplate(WechatUser::where('uid', $uid)->value('openid'), WechatTemplateService::USER_BALANCE_CHANGE, [
-                'first' => $mark,
-                'keyword1' => '佣金提现',
-                'keyword2' => date('Y-m-d H:i:s', time()),
-                'keyword3' => $extract_number,
-                'remark' => '错误原因:' . $fail_msg
-            ], Url::buildUrl('/user/cashrecord')->suffix('')->domain(true)->build());
-        } else if (strtolower($User['user_type']) == 'routine') {
-            RoutineTemplate::sendExtractFail($uid, $fail_msg, $extract_number, $User['nickname']);
-        }
-        return self::edit(compact('fail_time', 'fail_msg', 'status'), $id);
+        $res1 = UserBill::income('提现失败', $uid, 'now_money', 'extract', $extract_number, $id, bcadd($User['now_money'], $extract_number, 2), $mark);
+        $res2 = User::bcInc($uid, 'brokerage_price', $extract_number, 'uid');
+        // $extract_type = '未知方式';
+        // switch ($data['extract_type']) {
+        //     case 'alipay':
+        //         $extract_type = '支付宝';
+        //         break;
+        //     case 'bank':
+        //         $extract_type = '银行卡';
+        //         break;
+        //     case 'weixin':
+        //         $extract_type = '微信';
+        //         break;
+        // }
+        // if (strtolower($User['user_type']) == 'wechat') {
+        //     WechatTemplateService::sendTemplate(WechatUser::where('uid', $uid)->value('openid'), WechatTemplateService::USER_BALANCE_CHANGE, [
+        //         'first' => $mark,
+        //         'keyword1' => '佣金提现',
+        //         'keyword2' => date('Y-m-d H:i:s', time()),
+        //         'keyword3' => $extract_number,
+        //         'remark' => '错误原因:' . $fail_msg
+        //     ], Url::buildUrl('/user/cashrecord')->suffix('')->domain(true)->build());
+        // } else if (strtolower($User['user_type']) == 'routine') {
+        //     RoutineTemplate::sendExtractFail($uid, $fail_msg, $extract_number, $User['nickname']);
+        // }
+        $res3 = self::edit(compact('fail_time', 'fail_msg', 'status'), $id);
+        return $res1 && $res2 && $res3;
     }
 
     public static function changeSuccess($id)
     {
 
-        $data = self::get($id);
-        $extractNumber = $data['extract_price'];
-        $mark = '成功提现佣金' . $extractNumber . '元';
-        $wechatUserInfo = WechatUser::where('uid', $data['uid'])->field('openid,user_type,routine_openid,nickname')->find();
-        $extract_type = '未知方式';
-        switch ($data['extract_type']) {
-            case 'alipay':
-                $extract_type = '支付宝';
-                break;
-            case 'bank':
-                $extract_type = '银行卡';
-                break;
-            case 'weixin':
-                $extract_type = '微信';
-                break;
-        }
-        if ($wechatUserInfo) {
-            if (strtolower($wechatUserInfo->user_type) == 'routine') {
-                RoutineTemplate::sendExtractSuccess($data['uid'], $extractNumber, $wechatUserInfo->nickname);
-            } else if (strtolower($wechatUserInfo->user_type) == 'wechat') {
-                WechatTemplateService::sendTemplate($wechatUserInfo->openid, WechatTemplateService::USER_BALANCE_CHANGE, [
-                    'first' => $mark,
-                    'keyword1' => '佣金提现',
-                    'keyword2' => date('Y-m-d H:i:s', time()),
-                    'keyword3' => $extractNumber,
-                    'remark' => '点击查看我的佣金明细'
-                ], Url::buildUrl('/user/cashrecord')->suffix('')->domain(true)->build());
-            }
-        }
+        // $data = self::get($id);
+        // $extractNumber = $data['extract_price'];
+        // $mark = '成功提现佣金' . $extractNumber . '元';
+        // $wechatUserInfo = WechatUser::where('uid', $data['uid'])->field('openid,user_type,routine_openid,nickname')->find();
+        // $extract_type = '未知方式';
+        // switch ($data['extract_type']) {
+        //     case 'alipay':
+        //         $extract_type = '支付宝';
+        //         break;
+        //     case 'bank':
+        //         $extract_type = '银行卡';
+        //         break;
+        //     case 'weixin':
+        //         $extract_type = '微信';
+        //         break;
+        // }
+        // if ($wechatUserInfo) {
+        //     if (strtolower($wechatUserInfo->user_type) == 'routine') {
+        //         RoutineTemplate::sendExtractSuccess($data['uid'], $extractNumber, $wechatUserInfo->nickname);
+        //     } else if (strtolower($wechatUserInfo->user_type) == 'wechat') {
+        //         WechatTemplateService::sendTemplate($wechatUserInfo->openid, WechatTemplateService::USER_BALANCE_CHANGE, [
+        //             'first' => $mark,
+        //             'keyword1' => '佣金提现',
+        //             'keyword2' => date('Y-m-d H:i:s', time()),
+        //             'keyword3' => $extractNumber,
+        //             'remark' => '点击查看我的佣金明细'
+        //         ], Url::buildUrl('/user/cashrecord')->suffix('')->domain(true)->build());
+        //     }
+        // }
         return self::edit(['status' => EXTRACT_SUC], $id);
     }
 

+ 164 - 47
app/api/controller/user/UserExtractController.php

@@ -2,12 +2,13 @@
 
 namespace app\api\controller\user;
 
-use app\admin\model\system\SystemConfig;
-use app\models\store\StoreOrder;
 use app\models\user\UserBill;
 use app\models\user\UserExtract;
 use app\Request;
+use crmeb\services\payment\PaymentService;
 use crmeb\services\UtilService;
+use think\facade\Log;
+use tw\redis\UserRds;
 
 /**
  * 提现类
@@ -17,45 +18,135 @@ use crmeb\services\UtilService;
 class UserExtractController
 {
     /**
-     * 提现银行
-     * @param Request $request
-     * @return mixed
+     * @api {get} /extract/bank 提现银行
+     * @apiName ExtractBank
+     * @apiGroup Brokerage
+     * 
+     * @apiSuccess {float} broken_commission 冻结佣金
+     * @apiSuccess {float} brokerage_price 总佣金
+     * @apiSuccess {float} commissionCount  可提现佣金
+     * @apiSuccess {string[]} extractBank   银行列表
+     * @apiSuccess {float} minPrice 最小提现金额
      */
     public function bank(Request $request)
     {
         $user = $request->user();
         // 佣金冻结时间
-        $broken_time = intval(sys_config('extract_time'));
-        $search_time = time() - 86400 * $broken_time;
+        $frozen_days = intval(sys_config('extract_time'));
+        $frozen_from = time() - 86400 * $frozen_days;
         //可提现佣金
-        //返佣 +
         $brokerage_commission = UserBill::where(['uid' => $user['uid'], 'category' => 'now_money', 'type' => 'brokerage'])
-            ->where('add_time', '>', $search_time)
+            ->where('add_time', '>', $frozen_from)
             ->where('pm', 1)
             ->sum('number');
-        //退款退的佣金 -
         $refund_commission = UserBill::where(['uid' => $user['uid'], 'category' => 'now_money', 'type' => 'brokerage'])
-            ->where('add_time', '>', $search_time)
+            ->where('add_time', '>', $frozen_from)
             ->where('pm', 0)
             ->sum('number');
-        $data['broken_commission'] = bcsub($brokerage_commission, $refund_commission, 2);
-        if ($data['broken_commission'] < 0)
+        $data['broken_commission'] = floatval(bcsub($brokerage_commission, $refund_commission, 2));
+        if ($data['broken_commission'] < 0) {
             $data['broken_commission'] = 0;
-//        return $data;
+        }
+
         $data['brokerage_price'] = $user['brokerage_price'];
         //可提现佣金
-        $data['commissionCount'] = $data['brokerage_price'] - $data['broken_commission'];
+        $data['commissionCount'] = floatval(bcsub($data['brokerage_price'], $data['broken_commission'], 2));
         $extractBank = sys_config('user_extract_bank') ?? []; //提现银行
         $extractBank = str_replace("\r\n", "\n", $extractBank);//防止不兼容
         $data['extractBank'] = explode("\n", is_array($extractBank) ? (isset($extractBank[0]) ? $extractBank[0] : $extractBank) : $extractBank);
         $data['minPrice'] = sys_config('user_extract_min_price');//提现最低金额
+        // 输入记忆
+        $ur = new UserRds();
+        $cach = $ur->gets($user['uid'], ['wxpayName', 'bankCardNo', 'bankUser', 'bankName']);
+        $data['wxpayName']  = $cach['wxpayName'];
+        $data['bankCardNo'] = $cach['bankCardNo'];
+        $data['bankUser']   = $cach['bankUser'];
+        $data['bankName']   = $cach['bankName'];
+
         return app('json')->successful($data);
     }
 
     /**
-     * 提现申请
-     * @param Request $request
-     * @return mixed
+     * @api {post} /extract/bank_fee 查询银行提现手续费
+     * @apiName PostExtractBankFee
+     * @apiGroup Brokerage
+     * 
+     * @apiBody {int} extract_type 提现类型/通道
+     * @apiBody {float} money 提现金额
+     * 
+     * @apiSuccess {float} rate 手续费率
+     * @apiSuccess {float} min 最少收取
+     * @apiSuccess {float} max 最多收取
+     * @apiSuccess {float} fee 当前收取
+     * @apiSuccess {float} valid 实际到帐金额
+     */
+    public function bank_fee(Request $request)
+    {
+        $rate = 0.01;  // 1%
+        $min = 1;   //
+        $max = 25;  //
+
+        $extractInfo = UtilService::postMore([
+            ['alipay_code', ''],
+            ['extract_type', ''],
+            ['money', 0],
+            ['name', ''],
+            ['bankname', ''],
+            ['cardnum', ''],
+            ['weixin', ''],
+        ], $request);
+        $user = $request->user();
+
+
+        if ($extractInfo['extract_type'] != 'bank') {
+            return app('json')->fail('不是银行卡提现');
+        }
+        if ($extractInfo['money'] < sys_config('user_extract_min_price')) {
+            return app('json')->fail('金额小于最低提现金额');
+        }
+
+        // 手续费
+        $fee = floatval(bcmul($extractInfo['money'], 0.01, 2));
+        if ($fee < $min) {
+            $fee = $min;
+        }
+        if ($fee > $max) {
+            $fee = $max;
+        }
+        // 实际到帐金额
+        $valid = floatval(bcsub($extractInfo['money'], $fee, 2));
+        
+        return app('json')->successful([
+            'rate' => $rate,       // 费率
+            'min' => $min,         // 最少收费
+            'max' => $max,         // 最大收费
+            'fee' => $fee,         // 当前收费
+            'valid'=> $valid,      // 实际到帐
+        ]);
+    }
+
+    /**
+     * @api {post} /extract/cash 提现申请
+     * @apiName ExtractCash
+     * @apiGroup Brokerage
+     * 
+     * @apiBody {string} alipay_code 支付宝号码
+     * @apiBody {string="alipay","bank","weixin"} extract_type 提现类型
+     * @apiBody {float} money 提现金额
+     * @apiBody {string} bankName 开户行
+     * @apiBody {string} cardnum 卡号
+     * @apiBody {string} name 微信实名/银行户名/支付宝帐号
+     * @apiBody {string} weixin 微信实名
+     * 
+     * @apiSuccessExample:
+     * {
+     * 
+     * }
+     * @apiErrorExample:
+     * {
+     * 
+     * }
+     * 
      */
     public function cash(Request $request)
     {
@@ -68,37 +159,13 @@ class UserExtractController
             ['cardnum', ''],
             ['weixin', ''],
         ], $request);
-        if (!preg_match('/^(([1-9][0-9]*)|(([0]\.\d{1,2}|[1-9][0-9]*\.\d{1,2})))$/', $extractInfo['money'])) return app('json')->fail('提现金额输入有误');
-        //提现设置最低金额
-        if($extractInfo['money'] < sys_config('user_extract_min_price')) return app('json')->fail('金额小于最低提现金额');
         $user = $request->user();
-        $broken_time = intval(sys_config('extract_time'));
-        $search_time = time() - 86400 * $broken_time;
-        //可提现佣金
-        //返佣 +
-        $brokerage_commission = UserBill::where(['uid' => $user['uid'], 'category' => 'now_money', 'type' => 'brokerage'])
-            ->where('add_time', '>', $search_time)
-            ->where('pm', 1)
-            ->sum('number');
-        //退款退的佣金 -
-        $refund_commission = UserBill::where(['uid' => $user['uid'], 'category' => 'now_money', 'type' => 'brokerage'])
-            ->where('add_time', '>', $search_time)
-            ->where('pm', 0)
-            ->sum('number');
-        $data['broken_commission'] = bcsub($brokerage_commission, $refund_commission, 2);
-        if ($data['broken_commission'] < 0)
-            $data['broken_commission'] = 0;
-        $data['brokerage_price'] = $user['brokerage_price'];
-        //可提现佣金
-        $commissionCount = $data['brokerage_price'] - $data['broken_commission'];
-        if ($extractInfo['money'] > $commissionCount) return app('json')->fail('可提现佣金不足');
-        if (!$extractInfo['cardnum'] == '')
-            if (!preg_match('/^([1-9]{1})(\d{14}|\d{18})$/', $extractInfo['cardnum']))
-                return app('json')->fail('银行卡号输入有误');
-        if (UserExtract::userExtract($request->user(), $extractInfo)) {
+        
+        list($suc, $msg) = PaymentService::user_request_extract($user, $extractInfo);
+        if ($suc) {
             return app('json')->successful('申请提现成功!');
         } else {
-            return app('json')->fail(UserExtract::getErrorInfo('提现失败'));
+            return app('json')->fail($msg);
         }
     }
 
@@ -110,9 +177,59 @@ class UserExtractController
      * 1. 满足对应提现通道要求,微信支付:每个用户1天限1次,每次限200, 平台每日限额 200,000
      * 2. 15 日内必须有退款订单。且30日内退款订单金额 > 提现金额
      * 3. 其他基本需求(最低提现限额等等)
+     * 
+     * 不满足体现条件,则交给人工处理
+     * API 执行失败,则必须重新提交提现请求,因为人工操作也是失败。
+     * 如果API 返回 SYSTEMERROR, 则按照原订单参数重新提交。
      */
-    public function cash_now(Request $request) 
+    public function flash_cash(Request $request) 
     {
+        // 提交申请
+        $extractInfo = UtilService::postMore([
+            ['alipay_code', ''],
+            ['extract_type', ''],
+            ['money', 0],
+            ['name', ''],
+            ['bankname', ''],
+            ['cardnum', ''],
+            ['weixin', ''],
+        ], $request);
+        $user = $request->user();
+        
+        list($suc, $msg) = PaymentService::user_request_extract($user, $extractInfo);
+        if (!$suc) {
+            return app('json')->fail($msg);
+        }
+        $row = $msg;
+        $extractRow = UserExtract::getUserExtractInfo($user['uid'], $row['extract_type'], $row['add_time']);
+        if (!$extractRow) {
+            Log::error('Unbelievable error getUserExtractInfo(' . $user['uid'] . ' ' . $row['add_time']. ')');
+            return app('json')->fail('提交申请失败');
+        }
+        // 检查条件
+        list($suc, $msg) = PaymentService::user_extract_auto_conditions($user);
+        if (!$suc) {
+            // 条件不满足,不进一步处理,相当与交给人工处理
+            return app('json')->fail($msg);
+        }
+
+        // API
+        list($suc, $ec, $es) = PaymentService::extract_by_api($extractRow);
 
+        if (!$suc) {
+            // API 失败 直接拒绝,因为重试也会大概率失败
+            list($ok, $msg) = PaymentService::user_extract_reject($extractRow['id'], $es);
+            if (!$ok) {
+                errlog('user_extract_reject()' . $extractRow['id']);
+            }
+            return app('json')->fail($es);
+        } else {
+            list($ok, $msg) = PaymentService::user_extract_passed($extractRow);
+            if (!$ok) {
+                errlog('user_extract_passed()' . $extractRow['id']);
+            }
+            return app('json')->successful('提现成功');
+        }
     }
+
 }

+ 25 - 0
app/common.php

@@ -10,6 +10,8 @@
 // +----------------------------------------------------------------------
 
 use \think\facade\Config as ThinkConf;
+use \think\facade\Log;
+
 // 应用公共文件
 
 // 订单常量
@@ -51,6 +53,29 @@ define('UPLOAD_TENCENT_COS', 4);
 define('SECONDS_OF_ONEDAY', 86400);
 define('DS', DIRECTORY_SEPARATOR);
 
+/**
+ * 
+ */
+function errlog($log)
+{
+    return Log::error($log);
+}
+
+function warnlog($log)
+{
+    return Log::warning($log);
+}
+
+function debuglog($log)
+{
+    return Log::debug($log);
+}
+
+function infolog($log)
+{
+    return Log::info($log);
+}
+
 /**
  * 获取所有 幸运2021 活动及其子活动 ID 列表
  */

+ 39 - 4
app/models/user/UserExtract.php

@@ -11,6 +11,7 @@ use crmeb\basic\BaseModel;
 use crmeb\services\workerman\ChannelService;
 use crmeb\traits\ModelTrait;
 use think\facade\Log;
+use think\facade\Config;
 
 /**
  * TODO 用户提现
@@ -99,23 +100,44 @@ class UserExtract extends BaseModel
             $insertData['wechat'] = $userInfo['nickname'];
         }
         if($data['extract_type'] == 'alipay'){
+            $enabled = Config::get('app.extract_alipay_enabled', false);
+            if (!$enabled) {
+                return self::setErrorInfo('支付宝提现已关闭');
+            }
+
             if(!$data['alipay_code']) {
                 return self::setErrorInfo('请输入支付宝账号');
             }
             $insertData['alipay_code'] = $data['alipay_code'];
             $mark = '使用支付宝提现'.$insertData['extract_price'].'元';
         }else if($data['extract_type'] == 'bank'){
+            $enabled = Config::get('app.extract_bank_enabled', false);
+            if (!$enabled) {
+                return self::setErrorInfo('银行卡提现已关闭');
+            }
+
             if(!$data['cardnum']) {
                 return self::setErrorInfo('请输入银行卡账号');
             }
             if(!$data['bankname']) {
                 return self::setErrorInfo('请输入开户行信息');
             }
+            
             $mark = '使用银联卡'.$insertData['bank_code'].'提现'.$insertData['extract_price'].'元';
         }else if($data['extract_type'] == 'weixin'){
+            $enabled = Config::get('app.extract_weixin_enabled', false);
+            if (!$enabled) {
+                return self::setErrorInfo('微信提现已关闭');
+            }
+
             if(!$data['weixin']) {
                 return self::setErrorInfo('请输入微信账号');
             }
+
+            if ($insertData['extract_price'] > 200) {
+                return self::setErrorInfo('微信提现每次不能大于200元');
+            }
+
             $mark = '使用微信提现'.$insertData['extract_price'].'元';
         }
         
@@ -124,7 +146,7 @@ class UserExtract extends BaseModel
             $res1 = self::create($insertData);
             if(!$res1) {
                 Log::error('UserExtract.php line 101. insert failed.');
-                return self::setErrorInfo('提现失败,请联系客服处理');
+                return self::setErrorInfo('提现申请失败,请联系客服处理');
             }
 
             $res2 = User::edit(['brokerage_price'=>$balance],$userInfo['uid'],'uid');
@@ -140,14 +162,14 @@ class UserExtract extends BaseModel
             } else {
                 Log::error('UserExtract.php sql failed. $res2=' . $res2 . ' $res3=' . $res3);
                 self::rollbackTrans();
-                return self::setErrorInfo('提现失败,请联系客服处理');
+                return self::setErrorInfo('提现申请失败,请联系客服处理');
             }
             self::commitTrans();
-            return true;
+            return $insertData;
         }catch (\Exception $e){
             Log::error('UserExtract.php exception:' . $e->getMessage());
             self::rollbackTrans();
-            return self::setErrorInfo('提现失败,请联系客服处理');
+            return self::setErrorInfo('提现申请失败,请联系客服处理');
         }
     }
 
@@ -161,6 +183,19 @@ class UserExtract extends BaseModel
         return self::where(compact('uid'))->order('add_time DESC')->find();
     }
 
+    /**
+     * 根据条件查找
+     */
+    public static function getUserExtractInfo($uid, $extract_type, $time, $status=EXTRACT_AUDITING)
+    {
+        $res = self::where('uid', $uid)
+            ->where('extract_type', $extract_type)
+            ->where('add_time', $time)
+            ->where('status', $status)
+            ->find();
+        return $res;
+    }
+
     /**
      * 获得用户提现总金额
      * @param $uid

+ 1 - 1
config/activity.php

@@ -4,7 +4,7 @@ return [
     // 根分类ID
     'root_cate_id' => 10,
     // 是否开启挖矿活动
-    'mining_enabled' => true,
+    'mining_enabled' => false,
     'mining_display_name' => '黑洞星球',
     'mining_display_pic' => 'http://x.png',
     // 挖矿活动ID

+ 7 - 0
config/app.php

@@ -43,6 +43,13 @@ return [
     // 显示错误信息
     'show_error_msg'   => false,
 
+    // 部署的云服务器或 VPS IP 地址
+    'server_ip'        => '81.70.81.74',
+    // 微信提现开关
+    'extract_weixin_enabled'  => true,     
+    'extract_bank_enabled'    => false,
+    'extract_alipay_enabled'  => false,
+    
     /// 企业微信机器人
     // redis key @deprecated.
     'redis_robot_msg_key'   => 'qywechatpush',

+ 123 - 0
crmeb/payment/MachantPay.php

@@ -0,0 +1,123 @@
+<?php
+namespace crmeb\payment;
+
+
+use \Yurun\PaySDK\Weixin\Params\PublicParams;
+use \Yurun\PaySDK\Weixin\CompanyPay\Weixin\Pay\Request;
+use \Yurun\PaySDK\Weixin\SDK; // TODO: update to V3
+use crmeb\services\SystemConfigService;
+use EasyWeChat\Core\Exception;
+use \think\facade\Log;
+use \think\facade\Config;
+/**
+ * 企业付款
+ * 
+ * 包含微信/支付宝等付款到零钱,付款到银行卡
+ */
+class MachantPay {
+
+    /**
+     * 获得微信支付-付款到零钱/银行卡 API 用到的参数
+     * 
+     * DEPENDENCIES: 数据库读取
+     */
+    protected static function getWeixinParams() 
+    {
+        // 读取配置
+        $payment = SystemConfigService::more([
+            'pay_routine_appid',
+            'pay_routine_mchid', 
+            'pay_routine_key', 
+            'pay_routine_client_cert', 
+            'pay_routine_client_key', 
+            'pay_weixin_open',
+        ]);
+
+        if (!isset($payment['pay_weixin_open']) || !$payment['pay_weixin_open']) {
+            throw new Exception('weixin pay is not enabled.', -1);
+        }
+
+        $params = new PublicParams();
+        $params->appID = $payment['pay_routine_appid'] ?? '';
+        $params->mch_id = $payment['pay_routine_mchid'] ?? '';
+        $params->key = $payment['pay_routine_key'] ?? '';
+        $params->keyPath = realpath('.' . $payment['pay_routine_client_key']);
+        $params->certPath = realpath('.' . $payment['pay_routine_client_cert']);
+
+        return $params;
+    }
+
+    /**
+     * 付款到微信零钱
+     * 
+     * 文档见 (https://doc.yurunsoft.com/PaySDK/112)
+     * 
+     * NOTICE: 本函数只是调用微信 API,并未进行业务逻辑处理。
+     * 
+     * @param int $openid: wechat user openid
+     * @param string $trade_no: 付款订单号,平台自定义
+     * @param int $amount: 金额,单位为分
+     * @param string $desc: 订单描述
+     * @param string $realname: 收款放真实姓名, 参数 check_name 为 FORCE_CHECK 时使用。
+     * 
+     * @return (bool, int, string) (是否成功,错误代码,错误信息)
+     * 
+     * TODO: 微信付款升级为 V3(https://wechatpay-api.gitbook.io/wechatpay-api-v3/wei-xin-zhi-fu-api-v3-jie-kou-gui-fan)
+     * 本功能使用 Yurunsoft/PaySDK 也已支持 V3 (2021/11/28),但申请微信支付时未申请微信 V3 相关 Key。等待升级使用 V3 协议,或再做一个函数
+     */
+    public static function toWeixin($openid, $trade_no, $amount, $desc='', $realname='') 
+    {
+        try {
+            $caller_ip = Config::get('app.server_ip', '127.0.0.1');
+            $params = self::getWeixinParams();
+            $sdk = new SDK($params);
+            $req = new Request();
+            $req->partner_trade_no = $trade_no;
+            $req->openid = $openid;
+            $req->check_name = 'NO_CHECK';
+            $req->re_user_name = $realname;
+            $req->amount = intval(bcmul($amount, 100, 0));
+            $req->desc = $desc;
+            $req->spbill_create_ip = $caller_ip;   // 调用接口的机器IP, 这个可能微信用于验证
+
+            $res = $sdk->execute($req);
+            
+            return [
+                $sdk->checkResult($res),
+                $sdk->getErrorCode($res),
+                $sdk->getError($res),
+            ];
+        } catch (\Exception $e) {
+            Log::warning('exception:' . $e->getMessage());
+            return [false, $e->getCode(), $e->getMessage()];
+        }
+    }
+
+    /**
+     * 通过微信支付付款到银行卡
+     */
+    public static function toBankByWeixin() 
+    {
+        try {
+
+        } catch (\Exception $e) {
+
+        }
+    }
+
+    /**
+     * 付款到支付宝
+     */
+    public static function toAlipay() 
+    {
+
+    }
+
+    /**
+     * 通过支付宝付款到银行卡
+     */
+    public static function toBankByAlipay() 
+    {
+
+    }
+}

+ 11 - 8
crmeb/services/MiniProgramService.php

@@ -45,14 +45,17 @@ class MiniProgramService
             'token' => isset($wechat['wechat_token']) ? trim($wechat['wechat_token']) : '',
             'aes_key' => isset($wechat['wechat_encodingaeskey']) ? trim($wechat['wechat_encodingaeskey']) : ''
         ];
-        $config['payment'] = [
-            'app_id' => isset($payment['pay_routine_appid']) ? trim($payment['pay_routine_appid']) : '',
-            'merchant_id' => trim($payment['pay_routine_mchid']),
-            'key' => trim($payment['pay_routine_key']),
-            'cert_path' => realpath('.' . $payment['pay_routine_client_cert']),
-            'key_path' => realpath('.' . $payment['pay_routine_client_key']),
-            'notify_url' => $wechat['site_url'] . Url::buildUrl('/api/routine/notify')->suffix(false)->build()
-        ];
+        if (isset($payment['pay_weixin_open']) && $payment['pay_weixin_open'] == 1) {
+            $config['payment'] = [
+                'app_id' => isset($payment['pay_routine_appid']) ? trim($payment['pay_routine_appid']) : '',
+                'merchant_id' => trim($payment['pay_routine_mchid']),
+                'key' => trim($payment['pay_routine_key']),
+                'cert_path' => realpath('.' . $payment['pay_routine_client_cert']),
+                'key_path' => realpath('.' . $payment['pay_routine_client_key']),
+                'notify_url' => $wechat['site_url'] . Url::buildUrl('/api/routine/notify')->suffix(false)->build()
+            ];
+        }
+        
         return $config;
     }
 

+ 178 - 0
crmeb/services/payment/PaymentService.php

@@ -0,0 +1,178 @@
+<?php 
+namespace crmeb\services\payment;
+
+use app\models\user\WechatUser;
+use crmeb\payment\MachantPay;
+use app\models\user\UserBill;
+use app\models\user\UserExtract;
+use app\admin\model\user\UserExtract as UserExtractAdmin;
+use tw\redis\UserRds;
+
+/**
+ * 支付,提现相关业务逻辑,为了减少 Controller 部分的代码。
+ * Controller 的代码不容易测试。 
+ */
+class PaymentService 
+{
+    /**
+     * 执行用户申請提现
+     *
+     * @param Object $user
+     * @param array $extractInfo: 
+     * 
+     * ['alipay_code', ''],
+     * ['extract_type', ''],
+     * ['money', 0],
+     * ['name', ''],
+     * ['bankname', ''],
+     * ['cardnum', ''],
+     * ['weixin', ''],
+     * 
+     * @return [$ok, $err_msg]
+     * 
+     */
+    public static function user_request_extract($user, $extractInfo)
+    {
+        if (!preg_match('/^(([1-9][0-9]*)|(([0]\.\d{1,2}|[1-9][0-9]*\.\d{1,2})))$/', $extractInfo['money'])) {
+            return [false, '提现金额输入有误'];
+        }
+        // 最小提现额度
+        if($extractInfo['money'] < sys_config('user_extract_min_price')) {
+            return [false, '金额小于最低提现金额'];
+        }
+        // 佣金冻结天数
+        $frozen_days = intval(sys_config('extract_time'));
+        $valid_time = time() - 86400 * $frozen_days;
+        // 冻结期获得佣金
+        $brokerage_commission = UserBill::where(['uid' => $user['uid'], 'category' => 'now_money', 'type' => 'brokerage'])
+            ->where('add_time', '>', $valid_time)
+            ->where('pm', 1)
+            ->sum('number');
+        // 冻结期花费佣金
+        $refund_commission = UserBill::where(['uid' => $user['uid'], 'category' => 'now_money', 'type' => 'brokerage'])
+            ->where('add_time', '>', $valid_time)
+            ->where('pm', 0)
+            ->sum('number');
+        // 冻结佣金
+        $data['broken_commission'] = bcsub($brokerage_commission, $refund_commission, 2);
+        if ($data['broken_commission'] < 0) {
+            $data['broken_commission'] = 0;
+        }
+        // 总佣金
+        $data['brokerage_price'] = $user['brokerage_price'];
+        //可提现佣金
+        $commissionCount = bcsub($data['brokerage_price'], $data['broken_commission'], 2);
+        if ($extractInfo['money'] > $commissionCount) {
+            return [false, '可提现佣金不足'];
+        }
+        if (!$extractInfo['cardnum'] == '') {
+            if (!preg_match('/^([1-9]{1})(\d{14}|\d{18})$/', $extractInfo['cardnum'])) {
+                return [false, '银行卡号输入有误'];
+            }
+        }
+        $row = UserExtract::userExtract($user, $extractInfo);
+        if ($row) {
+            return [true, $row];
+        } else {
+            return [false, UserExtract::getErrorInfo('提现失败')];
+        }
+    }
+
+    /**
+     * 執行通過提現申請
+     */
+    public static function user_extract_passed($extractInfo)
+    {
+        // if (!UserExtractAdmin::be(['id' => $extractId, 'status' => EXTRACT_AUDITING])) {
+        //     return [false, '操作记录不存在或状态错误!'];
+        // }
+        if ($extractInfo['status'] == EXTRACT_SUC) {
+            return [false, '您已提现,请勿重复提现'];
+        }
+        if ($extractInfo['status'] == EXTRACT_FAILED) {
+            return [false, '您的提现申请已被拒绝'];
+        }
+
+        $res = UserExtractAdmin::changeSuccess($extractInfo['id']);
+        if ($res) {
+            event('UserExtractSucc', [$extractInfo]);
+            return [true, ''];
+        } else {
+            return [false, '操作失败'];
+        }
+    }
+
+    /**
+     * 拒絕提現申請
+     */
+    public static function user_extract_reject($extractId, $reason)
+    {
+        if (!UserExtractAdmin::be(['id' => $extractId, 'status' => EXTRACT_AUDITING])) {
+            return [false, '操作记录不存在或状态错误'];
+        }
+        $extract = UserExtractAdmin::get($extractId);
+        if (!$extract) {
+            return [false, '操作记录不存在'];
+        }
+        if ($extract->status == EXTRACT_SUC) {
+            return [false, '已经提现,错误操作'];
+        }
+        if ($extract->status == EXTRACT_FAILED) {
+            return [false, '您的提现申请已被拒绝,请勿重复操作'];
+        }
+        UserExtractAdmin::beginTrans();
+        $res = UserExtractAdmin::changeFail($extractId, $reason);
+        if ($res) {
+            UserExtractAdmin::commitTrans();
+            event('UserExtractRejected', [$extract, $reason]);
+            return [true, '操作成功'];
+        } else {
+            UserExtractAdmin::rollbackTrans();
+            return [false, '操作失败'];
+        }
+    }
+
+    /**
+     * 用戶自動提現需要滿足的條件
+     * 
+     * @param array $user:
+     * @return boolean:
+     */
+    public static function user_extract_auto_conditions($user)
+    {
+        // 微信支付提現到零錢:每個用戶每天提現 1 次,每次最多 200, 平臺每天限額 200,000.
+        return [true, ''];
+    }
+
+    /**
+     * 用户提现,集成支持的各种提现渠道
+     * 
+     * @param array $extractInfo: user_extract 行
+     */
+    public static function extract_by_api($extractInfo)
+    {
+        if (!$extractInfo) {
+            return [false, -10404, '记录不存在'];
+        }
+
+        $trade_no = md5($extractInfo['uid'] . $extractInfo['extract_price'] . $extractInfo['add_time']);
+
+        switch($extractInfo['extract_type']) {
+            case 'weixin':
+                $openid = WechatUser::where('uid', $extractInfo['uid'])->value('routine_openid');
+                //
+                $user = new UserRds();
+                $user->set($extractInfo['uid'], 'wxpayName', $extractInfo['real_name']);
+                //
+                return MachantPay::toWeixin($openid, $trade_no, $extractInfo['extract_price'], '佣金提现', $extractInfo['real_name']);
+            case 'bank':
+                // 记忆银行信息
+                break;
+            default:
+                // 其他情况不处理,返回失败
+                errlog('unbelievable error: extract_type='. $extractInfo['extract_type']);
+                return [false, -10000, '不支持的类型'];
+        } // switch
+    }
+
+}

+ 8 - 5
crmeb/subscribes/UserSubscribe.php

@@ -154,7 +154,7 @@ class UserSubscribe
     {
         list('user' => $user) = $event;
 
-        $res = UserBill::income('新用户送钱活动', $user['uid'], 'now_money', 'brokerage', $user['now_money'], $user['uid'], $user['now_money'], '新用户送' . $user['now_money'] . '元');
+        $res = UserBill::income('新用户送钱活动', $user['uid'], 'now_money', 'activity_1_gift', $user['now_money'], $user['uid'], $user['now_money'], '新用户送' . $user['now_money'] . '元');
         if (!$res) {
             Log::error('user ' . $user['uid'] . 'registered send money ' . $user['now_money'] . ' failed.');
         }
@@ -172,7 +172,10 @@ class UserSubscribe
     }
 
     /**
-     * 用戶提現 UserRequestWithdrawal
+     * 用戶申请提現 UserRequestWithdrawal
+     * 
+     * 插入异步任务,自动审核。效果等同于后台手动批准。
+     * 如果不满足自动提现条件,则不处理,发出机器人通知,交给人工审核
      */
     public function onUserRequestWithdrawal($event)
     {
@@ -195,9 +198,9 @@ class UserSubscribe
                 . '已转账,请耐心等待您的收款账户系统确认后核对金额。衷心感谢您对美天旺的信任和支持。如有问题,请联系客服。', $icon);
     }
     /**
-     * 後臺拒絕提現 UserExtractFail
+     * 後臺拒絕提現 UserExtractRejected
      */
-    public function onUserExtractFail($event)
+    public function onUserExtractRejected($event)
     {
         list($extract, $fail_msg) = $event;
         $icon = Config::get('app.header_cs_2', '');
@@ -205,7 +208,7 @@ class UserSubscribe
         UserNotice::sendNoticeTo($extract['uid'], '您的提现申请已处理',
         '您申请提现 ' . $extract['extract_price']
         . ' 元到' . $extract_type . '帐号' . $account
-        . '未能成功执行,原因:' . $fail_msg['message'] . '。请联系客服处理。', $icon);
+        . '未能成功执行,原因:' . $fail_msg . '。请联系客服。', $icon);
     }
 
     /**

+ 3 - 1
route/api/route.php

@@ -163,7 +163,9 @@ Route::group(function () {
     Route::get('integral/list', 'user.UserBillController/integral_list')->name('integralList');//积分记录
     //提现类
     Route::get('extract/bank', 'user.UserExtractController/bank')->name('extractBank');//提现银行/提现最低金额
-    Route::post('extract/cash', 'user.UserExtractController/cash')->name('extractCash');//提现申请
+    Route::post('extract/bank_fee', 'user.UserExtractController/bank_fee')->name('extractBankFee');//提现银行手续费查询
+    Route::post('extract/cash', 'user.UserExtractController/flash_cash')->name('extractCash');//提现申请
+    
     //充值类
     Route::post('recharge/routine', 'user.UserRechargeController/routine')->name('rechargeRoutine');//小程序充值
     Route::post('recharge/wechat', 'user.UserRechargeController/wechat')->name('rechargeWechat');//公众号充值