diff --git a/app/wechat/Service.php b/app/wechat/Service.php index eecede34a..526def8ad 100644 --- a/app/wechat/Service.php +++ b/app/wechat/Service.php @@ -19,7 +19,10 @@ namespace app\wechat; use app\wechat\command\Auto; use app\wechat\command\Fans; use app\wechat\service\AutoService; +use app\wechat\service\PaymentService; +use think\admin\extend\CodeExtend; use think\admin\Plugin; +use think\Request; /** * 组件注册服务 @@ -53,6 +56,16 @@ class Service extends Plugin $this->app->event->listen('WechatFansSubscribe', function ($openid) { AutoService::register($openid); }); + + // 注册支付通知路由 + $this->app->route->any('/plugin-wxpay-notify/:vars', function (Request $request) { + try { + $data = json_decode(CodeExtend::deSafe64($request->param('vars')), true); + return PaymentService::notify($data); + } catch (\Exception|\Error $exception) { + return 'Error: ' . $exception->getMessage(); + } + }); } /** @@ -80,6 +93,13 @@ class Service extends Plugin ['name' => '关注自动回复', 'icon' => 'layui-icon layui-icon-release', 'node' => "wechat/auto/index"], ], ], + [ + 'name' => '微信支付', + 'subs' => [ + ['name' => '微信支付行为', 'icon' => 'layui-icon layui-icon-rmb', 'node' => "wechat/payment.record/index"], + ['name' => '微信退款管理', 'icon' => 'layui-icon layui-icon-engine', 'node' => "wechat/payment.refund/index"], + ] + ] ]; } } \ No newline at end of file diff --git a/app/wechat/controller/api/Test.php b/app/wechat/controller/api/Test.php index e4e532055..cb9c37a57 100644 --- a/app/wechat/controller/api/Test.php +++ b/app/wechat/controller/api/Test.php @@ -17,6 +17,7 @@ namespace app\wechat\controller\api; use app\wechat\service\MediaService; +use app\wechat\service\PaymentService; use app\wechat\service\WechatService; use think\admin\Controller; use think\admin\extend\CodeExtend; @@ -32,6 +33,7 @@ class Test extends Controller { /** * 微信JSAPI支付二维码 + * @login true * @return \think\Response */ public function jsapiQrc(): Response @@ -42,6 +44,7 @@ class Test extends Controller /** * 显示网页授权二维码 + * @login true * @return \think\Response */ public function oauthQrc(): Response @@ -52,6 +55,7 @@ class Test extends Controller /** * 显示网页授权二维码 + * @login true * @return \think\Response */ public function jssdkQrc(): Response @@ -62,6 +66,7 @@ class Test extends Controller /** * 微信扫码支付模式一二维码显示 + * @login true * @return \think\Response */ public function scanOneQrc(): Response @@ -72,21 +77,24 @@ class Test extends Controller /** * 扫码支付模式二测试二维码 + * @login true * @return \think\Response - * @throws \WeChat\Exceptions\InvalidResponseException - * @throws \WeChat\Exceptions\LocalCacheException + * @throws \think\admin\Exception */ public function scanTwoQrc(): Response { - $result = WechatService::WePayOrder()->create([ - 'body' => '测试商品', - 'total_fee' => '1', - 'trade_type' => 'NATIVE', - 'notify_url' => sysuri('wechat/api.test/notify', [], false, true), - 'out_trade_no' => CodeExtend::uniqidNumber(18), - 'spbill_create_ip' => $this->request->ip(), - ]); - return $this->_buildQrcResponse($result['code_url']); + $code = CodeExtend::uniqidDate(18, 'TX'); + $result = PaymentService::create('', $code, "扫码支付测试 {$code}", '0.01', PaymentService::WECHAT_QRC, '0.01'); + return $this->_buildQrcResponse($result['params']['code_url']); + // $result = WechatService::WePayOrder()->create([ + // 'body' => '测试商品', + // 'total_fee' => '1', + // 'trade_type' => 'NATIVE', + // 'notify_url' => sysuri('wechat/api.test/notify', [], false, true), + // 'out_trade_no' => CodeExtend::uniqidNumber(18), + // 'spbill_create_ip' => $this->request->ip(), + // ]); + // return $this->_buildQrcResponse($result['code_url']); } /** @@ -156,50 +164,22 @@ class Test extends Controller /** * 微信JSAPI支付测试 - * @return string + * @return void|string * @throws \WeChat\Exceptions\InvalidResponseException * @throws \WeChat\Exceptions\LocalCacheException * @throws \think\admin\Exception */ - public function jsapi(): string + public function jsapi() { - $this->url = $this->request->url(true); - $this->pay = WechatService::WePayOrder(); - $user = WechatService::getWebOauthInfo($this->url); - if (empty($user['openid'])) return '

网页授权获取OPENID失败!

'; - // 生成预支付码 - $result = $this->pay->create([ - 'body' => '测试商品', - 'openid' => $user['openid'], - 'total_fee' => '1', - 'trade_type' => 'JSAPI', - 'notify_url' => sysuri('wechat/api.test/notify', [], false, true), - 'out_trade_no' => CodeExtend::uniqidDate(18), - 'spbill_create_ip' => $this->request->ip(), - ]); - // 数据参数格式化 - $resultJson = var_export($result, true); - $optionJson = json_encode($this->pay->jsapiParams($result['prepay_id']), JSON_UNESCAPED_UNICODE); - $configJson = json_encode(WechatService::getWebJssdkSign(), JSON_UNESCAPED_UNICODE); - return << - 当前用户OPENID: {$user['openid']} - \n\n--- 创建微信预支付码结果 ---\n {$resultJson} - \n\n--- JSAPI 及 H5 支付参数 ---\n {$optionJson} - - - - -HTML; + // 微信用户信息 + $this->user = WechatService::getWebOauthInfo($this->request->url(true)); + if (empty($this->user['openid'])) return '

网页授权获取OPENID失败!

'; + // 生成支付参数 + $oCode = CodeExtend::uniqidDate(18, 'TX'); + $this->result = PaymentService::create($this->user['openid'], $oCode, "JSAPI 支付测试 {$oCode}", '0.01', PaymentService::WECHAT_GZH); + $this->optionJson = json_encode($this->result['params'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $this->configJson = json_encode(WechatService::getWebJssdkSign(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $this->fetch(); } /** diff --git a/app/wechat/controller/payment/Record.php b/app/wechat/controller/payment/Record.php new file mode 100644 index 000000000..c2ead9ba3 --- /dev/null +++ b/app/wechat/controller/payment/Record.php @@ -0,0 +1,76 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-wechat +// | github 代码仓库:https://github.com/zoujingli/think-plugs-wechat +// +---------------------------------------------------------------------- + +namespace app\wechat\controller\payment; + +use app\wechat\model\WechatFans; +use app\wechat\model\WechatPaymentRecord; +use app\wechat\service\PaymentService; +use think\admin\Controller; +use think\admin\helper\QueryHelper; +use think\exception\HttpResponseException; + +/** + * 微信支付行为管理 + * @class Record + * @package app\wechat\controller + */ +class Record extends Controller +{ + /** + * 微信支付行为管理 + * @auth true + * @menu true + * @return void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function index() + { + WechatPaymentRecord::mQuery()->layTable(function () { + $this->title = '支付行为管理'; + }, function (QueryHelper $query) { + $db = WechatFans::mQuery()->like('openid|nickname#nickname')->db(); + if ($db->getOptions('where')) $query->whereRaw("openid in {$db->field('openid')->buildSql()}"); + $query->like('order_code|order_name#order')->dateBetween('create_time'); + $query->with(['bindFans'])->equal('payment_status'); + }); + } + + /** + * 创建退款申请 + * @auth true + * @return void + */ + public function refund() + { + try { + $data = $this->_vali(['code.require' => '支付号不能为空!']); + $recode = WechatPaymentRecord::mk()->where($data)->findOrEmpty(); + if ($recode->isEmpty()) $this->error('支付单不存在!'); + if ($recode->getAttr('payment_status') < 1) $this->error('支付单未完成支付!'); + $desc = "来自订单 {$recode['order_code']} 的退款!"; + sysoplog('微信支付退款', "支付单 {$data['code']} 发起退款!"); + [$state, $message] = PaymentService::refund($data['code'], $recode->getAttr('payment_amount'), $desc); + $state ? $this->success($message) : $this->error($message); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/wechat/controller/payment/Refund.php b/app/wechat/controller/payment/Refund.php new file mode 100644 index 000000000..f43b90702 --- /dev/null +++ b/app/wechat/controller/payment/Refund.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-wechat +// | github 代码仓库:https://github.com/zoujingli/think-plugs-wechat +// +---------------------------------------------------------------------- + +namespace app\wechat\controller\payment; + +use app\wechat\model\WechatFans; +use app\wechat\model\WechatPaymentRecord; +use app\wechat\model\WechatPaymentRefund; +use think\admin\Controller; +use think\admin\helper\QueryHelper; + +/** + * 支付退款管理 + * @class Refund + * @package app\wechat\controller + */ +class Refund extends Controller +{ + /** + * 支付退款管理 + * @auth true + * @menu true + * @return void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function index() + { + WechatPaymentRefund::mQuery()->layTable(function () { + $this->title = '支付退款管理'; + }, function (QueryHelper $query) { + $query->with(['record'])->like('code|refund_trade#refund'); + if (($this->get['order'] ?? '') . ($this->get['nickname'] ?? '') . ($this->get['payment'] ?? '') . ($this->get['refund'] ?? '') !== '') { + $db1 = WechatFans::mQuery()->field('openid')->like('openid|nickname#nickname')->db(); + $db2 = WechatPaymentRecord::mQuery()->like('order_code|order_name#order,code|payment_trade#payment'); + $db2->whereRaw("openid in {$db1->buildSql()}"); + $query->whereRaw("record_code in {$db2->field('code')->db()->buildSql()}"); + } + }); + } +} \ No newline at end of file diff --git a/app/wechat/model/WechatPaymentRecord.php b/app/wechat/model/WechatPaymentRecord.php new file mode 100644 index 000000000..59cac98c7 --- /dev/null +++ b/app/wechat/model/WechatPaymentRecord.php @@ -0,0 +1,87 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-wechat +// | github 代码仓库:https://github.com/zoujingli/think-plugs-wechat +// +---------------------------------------------------------------------- + +namespace app\wechat\model; + +use app\wechat\service\PaymentService; +use think\admin\Model; +use think\model\relation\HasOne; + +/** + * 微信支付行为模型 + * @class WechatPaymentRecord + * @package app\wechat\model + */ +class WechatPaymentRecord extends Model +{ + /** + * 关联用户粉丝数据 + * @return \think\model\relation\HasOne + */ + public function fans(): HasOne + { + return $this->hasOne(WechatFans::class, 'openid', 'openid'); + } + + /** + * 绑定用户粉丝数据 + * @return \think\model\relation\HasOne + */ + public function bindFans(): HasOne + { + return $this->fans()->bind([ + 'fans_headimg' => 'headimgurl', + 'fans_nickname' => 'nickname', + ]); + } + + /** + * 格式化输出时间格式 + * @param mixed $value + * @return string + */ + public function getCreateTimeAttr($value): string + { + return $value ? format_datetime($value) : ''; + } + + /** + * 格式化输出时间格式 + * @param mixed $value + * @return string + */ + public function getUpdateTimeAttr($value): string + { + return $value ? format_datetime($value) : ''; + } + + /** + * 格式化输出时间格式 + * @param mixed $value + * @return string + */ + public function getPaymentTimeAttr($value): string + { + return $value ? format_datetime($value) : ''; + } + + public function toArray(): array + { + $data = parent::toArray(); + $data['type_name'] = PaymentService::tradeTypeNames[$data['type']] ?? $data['type']; + return $data; + } +} \ No newline at end of file diff --git a/app/wechat/model/WechatPaymentRefund.php b/app/wechat/model/WechatPaymentRefund.php new file mode 100644 index 000000000..770060e44 --- /dev/null +++ b/app/wechat/model/WechatPaymentRefund.php @@ -0,0 +1,67 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-wechat +// | github 代码仓库:https://github.com/zoujingli/think-plugs-wechat +// +---------------------------------------------------------------------- + +namespace app\wechat\model; + +use think\admin\Model; +use think\model\relation\HasOne; + +/** + * 微信支付退款模型 + * @class WechatPaymentRefund + * @package app\wechat\model + */ +class WechatPaymentRefund extends Model +{ + /** + * 关联支付订单 + * @return \think\model\relation\HasOne + */ + public function record(): HasOne + { + return $this->hasOne(WechatPaymentRecord::class, 'code', 'record_code')->with('bindfans'); + } + + /** + * 格式化输出时间格式 + * @param mixed $value + * @return string + */ + public function getCreateTimeAttr($value): string + { + return $value ? format_datetime($value) : ''; + } + + /** + * 格式化输出时间格式 + * @param mixed $value + * @return string + */ + public function getUpdateTimeAttr($value): string + { + return $value ? format_datetime($value) : ''; + } + + /** + * 格式化输出时间格式 + * @param mixed $value + * @return string + */ + public function getRefundTimeAttr($value): string + { + return $value ? format_datetime($value) : ''; + } +} \ No newline at end of file diff --git a/app/wechat/service/MediaService.php b/app/wechat/service/MediaService.php index 60f2620d0..53cfa47b3 100644 --- a/app/wechat/service/MediaService.php +++ b/app/wechat/service/MediaService.php @@ -39,13 +39,13 @@ class MediaService extends Service /** * 通过图文ID读取图文信息 * @param mixed $id 本地图文ID - * @param array $map 额外的查询条件 + * @param mixed $map 额外的查询条件 * @return array * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ - public static function news($id, array $map = []): array + public static function news($id, $map = []): array { // 文章主体数据 $data = WechatNews::mk()->where(['id' => $id, 'is_deleted' => 0])->where($map)->findOrEmpty()->toArray(); diff --git a/app/wechat/service/PaymentService.php b/app/wechat/service/PaymentService.php new file mode 100644 index 000000000..6199808c2 --- /dev/null +++ b/app/wechat/service/PaymentService.php @@ -0,0 +1,398 @@ + +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-wechat +// | github 代码仓库:https://github.com/zoujingli/think-plugs-wechat +// +---------------------------------------------------------------------- + +declare (strict_types=1); + +namespace app\wechat\service; + +use app\wechat\model\WechatPaymentRecord; +use app\wechat\model\WechatPaymentRefund; +use think\admin\Exception; +use think\admin\extend\CodeExtend; +use think\admin\Library; +use think\Response; +use WePayV3\Order; + +/** + * 微信V3支付服务 + * @class PaymentService + * @package app\wechat\service + */ +class PaymentService +{ + // 微信支付类型 + public const WECHAT_APP = 'wechat_app'; + public const WECHAT_GZH = 'wechat_gzh'; + public const WECHAT_XCX = 'wechat_xcx'; + public const WECHAT_WAP = 'wechat_wap'; + public const WECHAT_QRC = 'wechat_qrc'; + + // 微信支付类型转换 + private const tradeTypes = [ + self::WECHAT_APP => 'APP', + self::WECHAT_WAP => 'MWEB', + self::WECHAT_GZH => 'JSAPI', + self::WECHAT_XCX => 'JSAPI', + self::WECHAT_QRC => 'NATIVE', + ]; + + // 微信支付类型名称 + public const tradeTypeNames = [ + self::WECHAT_APP => '微信APP支付', + self::WECHAT_WAP => '微信H5支付', + self::WECHAT_GZH => '服务号支付', + self::WECHAT_XCX => '小程序支付', + self::WECHAT_QRC => '二维码支付', + ]; + + /** + * 创建微信支付订单 + * @param string $openid 用户标识 + * @param string $oCode 订单单号 + * @param string $oName 订单标题 + * @param string $oAmount 订单金额(元) + * @param string $pType 支付类型 + * @param ?string $pAmount 支付金额(元) + * @param ?string $pRemark 支付描述 + * @return array + * @throws \think\admin\Exception + */ + public static function create(string $openid, string $oCode, string $oName, string $oAmount, string $pType, ?string $pAmount = null, ?string $pRemark = null): array + { + try { + // 检查订单是否完成 + if (self::isPayed($oCode, $oAmount)) { + return ['code' => 1, 'info' => '已完成支付!', 'data' => [], 'params' => []]; + } + // 检查剩余支付金额 + $leave = self::leave($oCode); + $pAmount = floatval(is_null($pAmount) ? (floatval($oAmount) - $leave) : $pAmount); + if ($leave + $pAmount > floatval($oAmount)) { + return ['code' => 0, 'info' => '支付总额超出!', 'data' => [], 'params' => []]; + } + $config = WechatService::getConfig(); + $pCode = CodeExtend::uniqidNumber(16, 'P'); + $data = [ + 'appid' => $config['appid'], + 'mchid' => $config['mch_id'], + 'payer' => ['openid' => $openid], + 'amount' => ['total' => intval($pAmount * 100), 'currency' => 'CNY'], + 'notify_url' => static::withNotifyUrl($pCode), + 'description' => empty($pRemark) ? $oName : ($oName . '-' . $pRemark), + 'out_trade_no' => $pCode, + ]; + $tradeType = static::tradeTypes[$pType] ?? ''; + if (in_array($pType, [static::WECHAT_WAP, static::WECHAT_QRC])) { + unset($data['payer']); + } + if ($pType === static::WECHAT_WAP) { + $tradeType = 'h5'; + $data['scene_info'] = ['h5_info' => ['type' => 'Wap'], 'payer_client_ip' => request()->ip()]; + } + $params = static::withPayment($config)->create(strtolower($tradeType), $data); + // 创建支付记录 + static::createPaymentAction($openid, $oCode, $oName, $oAmount, $pType, $pCode, strval($pAmount)); + // 返回支付参数 + return ['code' => 1, 'info' => '创建支付成功', 'data' => $data, 'params' => $params]; + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 查询微信支付订单 + * @param string $pCode 订单单号 + * @return array + */ + public static function query(string $pCode): array + { + try { + $result = static::withPayment()->query($pCode); + if (isset($result['trade_state']) && $result['trade_state'] === 'SUCCESS') { + $extra = [ + 'openid' => $result['payer']['openid'] ?? null, + 'payment_bank' => $result['bank_type'], + 'payment_time' => date('Y-m-d H:i:s', strtotime($result['success_time'])), + 'payment_remark' => $result['trade_state_desc'] ?? null, + 'payment_notify' => json_encode($result, 64 | 256), + ]; + $pAmount = strval($result['amount']['total'] / 100); + static::updatePaymentAction($result['out_trade_no'], $result['transaction_id'], $pAmount, $extra); + } + return $result; + } catch (\Exception $exception) { + return ['trade_state' => 'ERROR', 'trade_state_desc' => $exception->getMessage()]; + } + } + + /** + * 支付结果处理 + * @param array|null $data + * @return \think\Response + */ + public static function notify(?array $data = null): Response + { + try { + $notify = static::withPayment()->notify(); + $result = empty($notify['result']) ? [] : json_decode($notify['result'], true); + if (empty($result) || !is_array($result)) return response('error', 500); + //订单支付通知处理 + if ($data['scen'] === 'order' && isset($result['trade_state']) && $result['trade_state'] == 'SUCCESS') { + if ($data['order'] !== $result['out_trade_no']) return response('error', 500); + $extra = [ + 'openid' => $result['payer']['openid'] ?? null, + 'payment_bank' => $result['bank_type'], + 'payment_time' => date('Y-m-d H:i:s', strtotime($result['success_time'])), + 'payment_remark' => $result['trade_state_desc'] ?? null, + 'payment_notify' => json_encode($result, 64 | 256), + ]; + $pAmount = strval($result['amount']['payer_total'] / 100); + if (!static::updatePaymentAction($result['out_trade_no'], $result['transaction_id'], $pAmount, $extra)) { + return response('error', 500); + } + } elseif ($data['scen'] === 'refund' && isset($result['refund_status']) && $result['refund_status'] == 'SUCCESS') { + if ($data['order'] !== $result['out_refund_no']) return response('error', 500); + $refund = WechatPaymentRefund::mk()->where(['code' => $result['out_refund_no']])->findOrEmpty(); + if ($refund->isEmpty()) return response('error', 500); + $refund->save([ + 'refund_time' => date('Y-m-d H:i:s', strtotime($result['success_time'])), + 'refund_trade' => $result['refund_id'], + 'refund_scode' => $result['refund_status'], + 'refund_status' => 1, + 'refund_notify' => json_encode($result, 64 | 256), + 'refund_account' => $result['user_received_account'] ?? '', + ]); + static::refundSync($refund->getAttr('record_code')); + } + return response('success'); + } catch (\Exception $exception) { + return json(['code' => 'FAIL', 'message' => $exception->getMessage()])->code(500); + } + } + + /** + * 创建支付单退款 + * @param string $pcode 支付单号 + * @param string $amount 退款金额 + * @param string $reason 退款原因 + * @return array + * @throws \WeChat\Exceptions\InvalidResponseException + * @throws \WeChat\Exceptions\LocalCacheException + * @throws \think\admin\Exception + */ + public static function refund(string $pcode, string $amount, string $reason = ''): array + { + // 同步已退款状态 + $record = static::refundSync($pcode); + if ($record->getAttr('refund_amount') >= $record->getAttr('payment_amount')) { + return [1, '该订单已完成退款!']; + } + if ($record->getAttr('refund_amount') + floatval($amount) > $record->getAttr('payment_amount')) { + return [0, '退款大于支付金额!']; + } + // 创建支付退款申请 + do $rcode = CodeExtend::uniqidNumber(16, 'R'); + while (($model = WechatPaymentRefund::mk()->where(['code' => $rcode])->findOrEmpty())->isExists()); + // 初始化退款申请记录 + $model->save(['code' => $rcode, 'record_code' => $pcode, 'refund_amount' => $amount, 'refund_remark' => $reason]); + $options = [ + 'out_trade_no' => $pcode, + 'out_refund_no' => $rcode, + 'notify_url' => static::withNotifyUrl($rcode, 'refund'), + 'amount' => [ + 'refund' => intval(floatval($amount) * 100), + 'total' => intval($record->getAttr('payment_amount') * 100), + 'currency' => 'CNY' + ] + ]; + if (strlen($reason) > 0) $options['reason'] = $reason; + $result = static::withPayment()->createRefund($options); + if (in_array($result['code'] ?? $result['status'], ['SUCCESS', 'PROCESSING'])) { + return self::refundSyncByQuery($rcode); + } else { + return [0, $result['message'] ?? $result['status']]; + } + } + + /** + * 同步退款统计状态 + * @param string $pCode + * @return \app\wechat\model\WechatPaymentRecord + * @throws \WeChat\Exceptions\InvalidResponseException + * @throws \WeChat\Exceptions\LocalCacheException + * @throws \think\admin\Exception + */ + public static function refundSync(string $pCode): WechatPaymentRecord + { + $record = WechatPaymentRecord::mk()->where(['code' => $pCode])->findOrEmpty(); + if ($record->isEmpty()) throw new Exception('支付单不存在!'); + if ($record->getAttr('payment_status') < 1) throw new Exception("支付未完成!"); + // 最近一条记录,同步查询刷新 + $map = ['record_code' => $pCode]; + $last = WechatPaymentRefund::mk()->where($map)->order('id desc')->findOrEmpty(); + if ($last->isExists() && $last->getAttr('refund_status') === 0) { + static::refundSyncByQuery($last->getAttr('code')); + } + // 统计刷新退款金额 + $where = ['record_code' => $pCode, 'refund_status' => 1]; + $amount = WechatPaymentRefund::mk()->where($where)->sum('refund_amount'); + $record->save(['refund_amount' => $amount, 'refund_status' => intval($amount > 0)]); + return $record; + } + + /** + * 同步退款状态 + * @param string $rCode + * @return array + * @throws \WeChat\Exceptions\InvalidResponseException + * @throws \WeChat\Exceptions\LocalCacheException + * @throws \think\admin\Exception + */ + public static function refundSyncByQuery(string $rCode): array + { + $refund = WechatPaymentRefund::mk()->where(['code' => $rCode])->findOrEmpty(); + if ($refund->isEmpty()) return [0, '退款不存在!']; + if ($refund->getAttr('refund_status')) return [1, '退款已完成!']; + $result = static::withPayment()->queryRefund($rCode); + $extra = [ + 'refund_trade' => $result['refund_id'], + 'refund_scode' => $result['status'], + 'refund_status' => intval($result['status'] === 'SUCCESS'), + 'refund_notify' => json_encode($result, 64 | 256), + 'refund_account' => $result['user_received_account'] ?? '', + ]; + if (isset($result['success_time'])) { + $extra['refund_time'] = date('Y-m-d H:i:s', strtotime($result['success_time'])); + } + $refund->save($extra); + // 同步支付订单 + static::refundSync($refund->getAttr('record_code')); + if ($result['status'] === 'SUCCESS') return [1, '退款已完成!']; + if ($result['status'] === 'PROCESSING') return [1, '退款处理中!']; + return [0, $result['message'] ?? $result['status']]; + } + + /** + * 判断是否完成支付 + * @param string $oCode 原订单单号 + * @param string $oAmount 需支付金额 + * @return boolean + */ + public static function isPayed(string $oCode, string $oAmount): bool + { + return self::leave($oCode) >= $oAmount; + } + + /** + * 获取已支付金额 + * @param string $oCode + * @return float + */ + public static function leave(string $oCode): float + { + $where = ['order_code' => $oCode, 'payment_status' => 1]; + return WechatPaymentRecord::mk()->where($where)->sum('payment_amount'); + } + + /** + * 初始化支付实现 + * @param array|null $config + * @return \WePayV3\Order + * @throws \WeChat\Exceptions\InvalidResponseException + * @throws \WeChat\Exceptions\LocalCacheException + * @throws \think\admin\Exception + */ + protected static function withPayment(?array $config = null): Order + { + return Order::instance($config ?: WechatService::getConfig()); + } + + /** + * 获取支付通知地址 + * @param string $order 订单单号 + * @param string $scene 支付场景 + * @param array $extra + * @return string + */ + protected static function withNotifyUrl(string $order, string $scene = 'order', array $extra = []): string + { + $data = ['scen' => $scene, 'order' => $order]; + $vars = CodeExtend::enSafe64(json_encode($extra + $data, 64 | 256)); + return sysuri('@plugin-wxpay-notify', [], false, true) . "/{$vars}"; + } + + /** + * 创建支付行为 + * @param string $openid 用户编号 + * @param string $oCode 订单单号 + * @param string $oName 订单标题 + * @param string $oAmount 订单总金额 + * @param string $pType 支付平台 + * @param string $pCode 子支付单号 + * @param string $pAmount 子支付金额 + * @return array + * @throws \think\admin\Exception + */ + protected static function createPaymentAction(string $openid, string $oCode, string $oName, string $oAmount, string $pType, string $pCode, string $pAmount): array + { + // 检查是否已经支付 + $leave = static::leave($oCode); + if ($leave >= floatval($oAmount)) { + throw new Exception("订单 {$oCode} 已经完成支付!", 1); + } + if ($leave + floatval($pAmount) > floatval($oAmount)) { + throw new Exception('总支付金额大于订单金额!', 0); + } + $map = ['order_code' => $oCode, 'payment_status' => 1]; + $model = WechatPaymentRecord::mk()->where($map)->findOrEmpty(); + if ($model->isExists()) throw new Exception("订单 {$oCode} 已经完成支付!", 1); + // 写入订单支付行为 + $model->save([ + 'type' => $pType, + 'code' => $pCode, + 'appid' => WechatService::getAppid(), + 'openid' => $openid, + 'order_code' => $oCode, + 'order_name' => $oName, + 'order_amount' => $oAmount, + ]); + return $model->toArray(); + } + + /** + * 更新创建支付行为 + * @param string $pCode 商户订单单号 + * @param string $pTrade 平台交易单号 + * @param string $pAmount 实际到账金额 + * @param array $extra 订单扩展数据 + * @return boolean|array + */ + protected static function updatePaymentAction(string $pCode, string $pTrade, string $pAmount, array $extra = []) + { + // 更新支付记录 + $model = WechatPaymentRecord::mk()->where(['code' => $pCode])->findOrEmpty(); + if ($model->isEmpty()) return false; + // 更新支付行为 + foreach ($extra as $k => $v) if (is_null($v)) unset($extra[$k]); + $model->save($extra + ['payment_trade' => $pTrade, 'payment_status' => 1, 'payment_amount' => $pAmount]); + // 触发支付成功事件 + Library::$sapp->event->trigger('WechatPaymentSuccess', $model->refresh()->toArray()); + // 返回支付行为数据 + return $model->toArray(); + } +} \ No newline at end of file diff --git a/app/wechat/view/api/test/jsapi.html b/app/wechat/view/api/test/jsapi.html new file mode 100644 index 000000000..46f332180 --- /dev/null +++ b/app/wechat/view/api/test/jsapi.html @@ -0,0 +1,66 @@ + + + + + 微信 JSAPI 支付测试 + + + + + + +
+

1. 用户 OPENID

+
+
{$user['openid']}
+
+
+

2. 微信 JSAPI 支付参数

+
+
{:json_encode($result,64|128|256)}
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/app/wechat/view/config/payment.html b/app/wechat/view/config/payment.html index 83b2f572f..46901210f 100644 --- a/app/wechat/view/config/payment.html +++ b/app/wechat/view/config/payment.html @@ -13,7 +13,7 @@
-
+