chore: 添加静态资源文件和版本配置文件

添加大量图片、字体、音频等静态资源文件,包括用户界面图片、模板文件和安装界面素材
添加系统版本配置文件(.version)和常量定义文件(.constant)
更新供应商依赖文件,包括Symfony VarDumper组件和相关许可证文件
添加验证码配置文件(captcha.php)和.gitignore规则
This commit is contained in:
2026-03-06 20:14:23 +08:00
commit c9a98f342c
4485 changed files with 490986 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use think\helper\Str;
use EasyWeChat\Work\Application;
use Symfony\Component\HttpFoundation\HeaderBag;
use EasyWeChat\Pay\Application as PayApplication;
use EasyWeChat\MiniApp\Application as MiniAppApplication;
use crmeb\services\wechat\contract\BaseApplicationInterface;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use EasyWeChat\OfficialAccount\Application as OfficialAccountApplication;
use think\Request;
/**
* Class BaseApplication
* @package crmeb\services\wechat
*/
abstract class BaseApplication implements BaseApplicationInterface
{
//app端
const APP = 'app';
//h5端、公众端
const WEB = 'web';
//小程序端
const MINI = 'mini';
//开发平台
const OPEN = 'open';
//pc端
const PC = 'pc';
const BASE_URL = 'https://api.weixin.qq.com';
/**
* 访问端
* @var string
*/
protected string $accessEnd = '';
/**
*
* @var string
*/
protected string $name;
/**
* @var array
*/
protected static array $property = [];
/**
* @var string
*/
protected string $pushMessageHandler;
/**
* Debug
* @var bool
*/
protected bool $debug = true;
/**
* 设置消息处理类
* @param string $handler
* @return $this
*/
public function setPushMessageHandler(string $handler): static
{
$this->pushMessageHandler = $handler;
return $this;
}
/**
* 设置访问端
* @param string $accessEnd
* @return $this
*/
public function setAccessEnd(string $accessEnd)
{
if (in_array($accessEnd, [self::APP, self::WEB, self::MINI])) {
$this->accessEnd = $accessEnd;
}
return $this;
}
/**
* 自动获取访问端
* @param Request $request
* @return string
*/
public function getAuthAccessEnd(Request $request): string
{
if (!$this->accessEnd) {
try {
if ($request->isApp()) {
$this->accessEnd = self::APP;
} else if ($request->isPc()) {
$this->accessEnd = self::PC;
} else if ($request->isWechat() || $request->isH5()) {
$this->accessEnd = self::WEB;
} else if ($request->isRoutine()) {
$this->accessEnd = self::MINI;
} else {
$this->accessEnd = self::WEB;
}
} catch (\Throwable $e) {
$this->accessEnd = self::WEB;
}
}
return $this->accessEnd;
}
/**
* 记录错误日志
* @param \Throwable $e
*/
protected static function error(\Throwable $e)
{
static::instance()->debug && response_log_write([
'error' => $e->getMessage(),
'line' => $e->getLine(),
'file' => $e->getFile(),
'trace' => $e->getTrace()
]);
}
/**
* 请求日志
* @param string $message
* @param $request
* @param $response
*/
protected static function logger(string $message, $request, $response)
{
$debug = static::instance()->debug;
if ($debug) {
if (is_object($response) && method_exists($response, 'toArray')) {
$response = $response->toArray();
}
response_log_write([
'message' => $message,
'request' => $request,
'response' => $response
], 'info');
}
}
/**
* 设置request
* @param PayApplication|OfficialAccountApplication|MiniAppApplication|Application $application
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
protected function setRequest(PayApplication|OfficialAccountApplication|MiniAppApplication|Application $application)
{
$request = request();
$symfonyRequest = new SymfonyRequest($request->get(), $request->post(), [], $request->cookie(), [], $request->server(), $request->getContent());
$symfonyRequest->headers = new HeaderBag($request->header());
$application->setRequestFromSymfonyRequest($symfonyRequest);
}
/**
* 设置日志
* @param MiniAppApplication|OfficialAccountApplication $loggerAwareTrait
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function setLogger(MiniAppApplication|OfficialAccountApplication $loggerAwareTrait)
{
$loggerAwareTrait->setLogger(app()->log);
}
/**
* 设置缓存
* @param $withCache Application|OfficialAccountApplication|MiniAppApplication|
* @return void
*/
protected function setCache($withCache)
{
$withCache->setCache(app()->cache);
}
/**
* 设置http请求
* @param $application Application|OfficialAccountApplication|MiniAppApplication|PayApplication
* @param string $baseUrl
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
protected function setHttpClient($application, string $baseUrl = self::BASE_URL)
{
$application->setHttpClient(new CustomHttpClient($baseUrl));
}
/**
* 获取请求驱动
* @param string $name
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getClientClassName(string $name)
{
$class = Str::studly($name) . 'Client';
$className = '\\crmeb\\services\\wechat\\client\\' . $this->name . '\\' . $class;
return $className;
}
/**
* @param $name
* @param $arguments
* @return mixed|object
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function __call($name, $arguments)
{
$className = $this->getClientClassName($name);
if (class_exists($className)) {
$client = $this->application()->getClient();
return new $className($client);
} else {
throw new WechatException('请求类不存在');
}
}
}

View File

@@ -0,0 +1,167 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Swoole\Coroutine\Http\Client;
/**
* 携程curl请求
* Class CustomHttpClient
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/24
* @package crmeb\services\wechat
*/
class CustomHttpClient implements HttpClientInterface
{
/**
* @var string
*/
protected string $baseUrl = '';
/**
* @var int
*/
protected int $timeout = 3;
/**
* @var array
*/
protected array $httpConfig = [];
/**
* CustomHttpClient constructor.
* @param string $baseUrl
* @param array $httpConfig
*/
public function __construct(string $baseUrl, array $httpConfig = [])
{
$this->baseUrl = $baseUrl;
$this->httpConfig = $httpConfig;
}
/**
* 发起请求
* @param string $method
* @param string $url
* @param array $options
* @return ResponseInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/24
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
// 合并传入的选项和默认选项
$options = array_merge(self::OPTIONS_DEFAULTS, $this->httpConfig, $options);
$ssl = strstr($this->baseUrl, 'https://') !== false;
$baseUrl = str_replace(['https://', 'http://', '/'], '', $this->baseUrl);
// 使用 Swoole Client 完成请求
$client = new Client($baseUrl, $ssl ? 443 : 80, $ssl);
$client->set([
'timeout' => !empty($options['timeout']) ? $options['timeout'] : $this->timeout,
'ssl_cert_file' => $options['cert'] ?? null,
'ssl_key_file' => $options['ssl_key'] ?? null
]);
$client->setMethod($method);
$headers = [];
// 设置请求头
foreach ($options['headers'] as $key => $value) {
if (is_string($key)) {
$values = [];
if (is_array($value)) {
foreach ($value as $item) {
[$type, $val] = strstr($item, ':') !== false ? explode(':', $item) : [null, null];
if ($type && $val) {
$values[] = $val;
}
}
$headers[$key] = implode(',', $values);
} else {
$values[] = $value;
$headers[$key] = implode(',', $values);
}
} else {
[$type, $val] = strstr($value, ':') !== false ? explode(':', $value) : [null, null];
$values[] = $val;
$headers[$type] = implode(',', $values);
}
}
$client->setHeaders($headers);
if (!empty($options['query'])) {
$url = $url . (strstr($url, '?') !== false ? '&' : '?') . http_build_query($options['query']);
}
if (!empty($options['json'])) {
$client->setData($options['json']);
} else if (!empty($options['body'])) {
$client->setData($options['body']);
}
// 发起请求
$client->execute('/' . $url);
// 创建响应对象
$response = new SwooleResponse($client);
// 关闭客户端连接
$client->close();
return $response;
}
/**
* @param iterable|ResponseInterface $responses
* @param float|null $timeout
* @return ResponseStreamInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
public function stream(iterable|ResponseInterface $responses, float $timeout = null): ResponseStreamInterface
{
// 创建 ResponseStreamInterface 对象并进行流式处理
$stream = new AsyncContext($responses, $this->httpClient, $timeout);
// 返回流对象
return $stream;
}
/**
* @param array $options
* @return $this
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
public function withOptions(array $options): static
{
$this->httpConfig = array_merge(self::OPTIONS_DEFAULTS, $options);
if (!empty($this->httpConfig['base_uri']) && $this->httpConfig['base_uri'] !== $this->baseUrl) {
$this->baseUrl = $this->httpConfig['base_uri'];
}
return $this;
}
}

View File

@@ -0,0 +1,259 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use think\facade\Config;
/**
* 默认配置
* Class DefaultConfig
* @package crmeb\services\wechat
*/
class DefaultConfig
{
//小程序appid
const MINI_APPID = 'mini.appid';
//公众号appid
const OFFICIAL_APPID = 'official.appid';
//开放平台appid
const APP_APPID = 'app.appid';
//开放平台网页端appid
const WEB_APPID = 'web.appid';
//企业微信id
const WORK_CORP_ID = 'work.corp_id';
//商户id
const PAY_MCHID = 'pay.mchid';
//系统配置域名地址,携带,格式:http://www.a.com
const COMMENT_URL = 'comment.url';
/**
*
*/
const WECHAT_CONFIG = [
//请求响应日志
'logger' => true,
//公用
'comment' => [
'url' => [
'key' => 'site_url'
],
],
//小程序配置
'mini' => [
'appid' => [
'key' => 'routine_appId'
],
'secret' => [
'key' => 'routine_appsecret'
],
'token' => [
'key' => 'wechat_token'
],
'key' => [
'key' => 'wechat_encodingaeskey'
],
'notifyUrl' => [
//必须携带斜杠开头
'value' => '/api/pay/notify/routine'
],
],
//公众号配置
'official' => [
'appid' => [
'key' => 'wechat_appid'
],
'secret' => [
'key' => 'wechat_appsecret'
],
'token' => [
'key' => 'wechat_token'
],
'key' => [
'key' => 'wechat_encodingaeskey'
],
'encode' => [
'key' => 'wechat_encode'
],
],
//开放平台APP
'app' => [
'appid' => [
'key' => 'wechat_app_appid'
],
'secret' => [
'key' => 'wechat_app_appsecret'
],
'token' => [
'key' => 'wechat_openapp_app_token'
],
'key' => [
'key' => 'wechat_openapp_app_aes_key'
],
'notifyUrl' => [
//必须携带斜杠开头
'value' => '/api/pay/notify/app'
],
],
//开放平台网页应用
'web' => [
'appid' => [
'key' => 'wechat_open_app_id'
],
'secret' => [
'key' => 'wechat_open_app_secret'
],
'token' => [
'key' => 'wechat_open_app_token'
],
'key' => [
'key' => 'wechat_open_app_aes_key'
],
],
//企业微信
'work' => [
'corp_id' => [
'key' => 'wechat_work_corpid'
],
'token' => [
'key' => 'wechat_work_token'
],
'key' => [
'key' => 'wechat_work_aes_key'
],
],
//支付
'pay' => [
//商户号
'mchid' => [
'key' => 'pay_weixin_mchid'
],
//小程序商户号
'routine_mchid' => [
'key' => 'pay_routine_mchid'
],
//支付key
'key' => [
'key' => 'pay_weixin_key'
],
//证书
'client_cert' => [
'key' => 'pay_weixin_client_cert'
],
//证书
'client_key' => [
'key' => 'pay_weixin_client_key'
],
'notifyUrl' => [
//支付回调,必须携带斜杠开头
'value' => '/api/pay/notify/wechat'
],
'refundUrl' => [
//退款回调,必须携带斜杠开头
'value' => '/api/pay/refund/wechat'
],
],
//v3支付新增配置,证书和商户号使用v2支付配置的证书
'v3_pay' => [
'key' => [
//默认使用value值,没有值使用eb_system_config配置中的key的值
'key' => 'v3_pay_weixin_key',
//配置值
'value' => '',
],
'serial_no' => [
//默认使用value值,没有值使用eb_system_config配置中的key的值
'key' => 'pay_weixin_serial_no',
//配置值
'value' => '',
],
'pay_type' => [
//默认使用value值,没有值使用eb_system_config配置中的key的值
'key' => 'pay_wechat_type',
//配置值
'value' => '',
],
'public_key'=>[
//默认使用value值,没有值使用eb_system_config配置中的key的值
'key' => 'v3_pay_public_key',
//配置值
'value' => '',
],
'public_pem'=>[
//默认使用value值,没有值使用eb_system_config配置中的key的值
'key' => 'v3_pay_public_pem',
//配置值
'value' => '',
],
],
];
/**
* 获取配置,如果配置为数组则使用value的值,如果没有值返回key
* @param string $key
* @return array|mixed|string[]|null|bool
*/
public static function value(string $key)
{
$config = [];
if (Config::has('wechat')) {
$config = Config::get('wechat', []);
}
$config = array_merge(self::WECHAT_CONFIG, $config);
$key = explode('.', $key);
$value = null;
foreach ($key as $k) {
if ($value) {
$value = $value[$k] ?? null;
} else {
$value = $config[$k] ?? null;
}
}
if (is_array($value)) {
return $value['value'] ?? null;
} else {
return $value;
}
}
/**
* @param string $key
* @return mixed|null
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
*/
public static function key(string $key)
{
$config = [];
if (Config::has('wechat')) {
$config = Config::get('wechat', []);
}
$config = array_merge(self::WECHAT_CONFIG, $config);
$key = explode('.', $key);
$value = null;
foreach ($key as $k) {
if ($value) {
$value = $value[$k] ?? null;
} else {
$value = $config[$k] ?? null;
}
}
if (is_array($value)) {
$value = $value['key'] ?? null;
}
return $value;
}
}

View File

@@ -0,0 +1,140 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use crmeb\utils\ApiErrorCode;
/**
* 错误消息处理
* Class ErrorMessage
* @package crmeb\services\wechat
*/
class ErrorMessage
{
const MSG_CODE = [
'1' => '未创建直播间',
'1003' => '商品id不存在',
'47001' => '入参格式不符合规范',
'200002' => '入参错误',
'300001' => '禁止创建/更新商品 或 禁止编辑&更新房间',
'300002' => '名称长度不符合规则',
'300006' => '图片上传失败',
'300022' => '此房间号不存在',
'300023' => '房间状态 拦截',
'300024' => '商品不存在',
'300025' => '商品审核未通过',
'300026' => '房间商品数量已经满额',
'300027' => '导入商品失败',
'300028' => '房间名称违规',
'300029' => '主播昵称违规',
'300030' => '主播微信号不合法',
'300031' => '直播间封面图不合规',
'300032' => '直播间分享图违规',
'300033' => '添加商品超过直播间上限',
'300034' => '主播微信昵称长度不符合要求',
'300035' => '主播微信号不存在',
'300036' => '主播微信号未实名认证',
'300037' => '购物直播频道封面图不合规',
'300038' => '未在小程序管理后台配置客服',
'9410000' => '直播间列表为空',
'9410001' => '获取房间失败',
'9410002' => '获取商品失败',
'9410003' => '获取回放失败',
'300003' => '价格输入不合规',
'300004' => '商品名称存在违规违法内容',
'300005' => '商品图片存在违规违法内容',
'300007' => '线上小程序版本不存在该链接',
'300008' => '添加商品失败',
'300009' => '商品审核撤回失败',
'300010' => '商品审核状态不对',
'300011' => '操作非法',
'300012' => '没有提审额度',
'300013' => '提审失败',
'300014' => '审核中,无法删除',
'300017' => '商品未提审',
'300018' => '图片尺寸不符合要求',
'300021' => '商品添加成功,审核失败',
'40001' => 'AppSecret错误或者AppSecret不属于这个小程序,请确认AppSecret 的正确性',
'40002' => '请确保grant_type字段值为client_credential',
'40013' => '不合法的AppID,请检查AppID的正确性,避免异常字符,注意大小写',
'40125' => '小程序配置无效,请检查配置',
'40164' => 'IP地址不在白名单中,请检查设置',
'41002' => '缺少appid参数',
'41004' => '缺少secret参数',
'43104' => 'appid与openid不匹配',
'48001' => '微信接口暂无权限,请先去获取',
'-1' => '系统错误',
];
const WORK_ERROR_MESSAGE = [
40098 => '成员尚未实名认证',
40068 => '不合法的标签/标签组ID'
];
/**
* 处理返回错误信息友好提示
* @param string $message
* @return array|mixed|string
*/
public static function getMessage(string $message)
{
if (strstr($message, 'Request AccessToken fail') !== false || strstr($message, 'Request access_token fail') !== false) {
$message = str_replace('Request AccessToken fail. response:', '', $message);
$message = str_replace('Request access_token fail:', '', $message);
$message = trim($message);
$message = json_decode($message, true) ?: [];
$errcode = $message['errcode'] ?? false;
if ($errcode) {
$message = ApiErrorCode::ERROR_WECHAT_MESSAGE[$errcode] ?? $message;
}
}
return $message;
}
/**
* 解析错误
* @param \Throwable $e
* @return array|mixed|string
*/
public static function getValidMessgae(\Throwable $e)
{
$message = '';
if (!isset(self::MSG_CODE[$e->getCode()]) && (strstr($e->getMessage(), 'Request AccessToken fail') !== false || strstr($e->getMessage(), 'Request access_token fail') !== false)) {
$message = str_replace('Request AccessToken fail. response:', '', $e->getMessage());
$message = str_replace('Request access_token fail:', '', $message);
$message = trim($message);
$message = json_decode($message, true) ?: [];
$errcode = $message['errcode'] ?? false;
if ($errcode) {
$message = self::MSG_CODE[$errcode] ?? $message;
}
}
return $message ?: self::MSG_CODE[$e->getCode()] ?? $e->getMessage();
}
/**
* 获取企业微信错误提示
* @param int $errcode
* @param string|null $message
* @return string|null
*/
public static function getWorkMessage(int $errcode, string $message = null)
{
if (isset(self::WORK_ERROR_MESSAGE[$errcode])) {
return self::WORK_ERROR_MESSAGE[$errcode];
} else {
return $message;
}
}
}

View File

@@ -0,0 +1,197 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use crmeb\services\wechat\message\Article;
use crmeb\services\wechat\message\Image;
use crmeb\services\wechat\message\Media;
use crmeb\services\wechat\message\News;
use crmeb\services\wechat\message\NewsItem;
use crmeb\services\wechat\message\Text;
use crmeb\services\wechat\message\TextCard;
use crmeb\services\wechat\message\Transfer;
use crmeb\services\wechat\message\Video;
use crmeb\services\wechat\message\Voice;
/**
* 消息回复模板
* Class Messages
* @package crmeb\services\wechat
*/
class Messages
{
/**
* 回复文本消息
* @param string $content 文本内容
* @return Text
*/
public static function textMessage(string $content = ''): Text
{
$content = str_replace("\\n", "\n", $content); //返回数据处理换行符
$content = str_replace("\\r", "\r", $content); //返回数据处理换行符
return new Text($content);
}
/**
* 回复图片消息
* @param string $mediaId 媒体资源 ID
* @return Image
*/
public static function imageMessage(string $mediaId): Image
{
return new Image($mediaId);
}
/**
* 回复视频消息
* @param string $mediaId 媒体资源 ID
* @param string $title 标题
* @param string $description 描述
* @param null $thumb_media_id 封面资源 ID
* @return Video
*/
public static function videoMessage(string $mediaId, string $title = '', string $description = '...', $thumb_media_id = null): Video
{
return new Video($mediaId, compact('title', 'description', 'thumb_media_id'));
}
/**
* 回复声音消息
* @param string $mediaId 媒体资源 ID
* @return Voice
*/
public static function voiceMessage(string $mediaId): Voice
{
return new Voice($mediaId);
}
/**
* 回复图文消息
* @param array|string $title
* @param string $description
* @param string $url
* @param string $image
* @return News
*/
public static function newsMessage(array|string $title, string $description = '...', string $url = '', string $image = ''): News
{
if (is_array($title)) {
$items = [
new NewsItem([
'title' => $title['title'],
'description' => $title['description'],
'url' => $title['url'],
'image' => $title['image']
])
];
} else {
$items = [
new NewsItem([
'title' => $title,
'description' => $description,
'url' => $url,
'image' => $image
])
];
}
return new News($items);
}
/**
* 创建新闻消息类型
* @param string|array $title
* @param string $description
* @param string $url
* @param string $image
* @return News
*/
public static function newMessage(string|array $title, string $description = '...', string $url = '', string $image = ''): News
{
if (is_array($title)) {
$items = [
new NewsItem([
'title' => $title['title'],
'description' => $title['description'],
'url' => $title['url'],
'image' => $title['image']
])
];
} else {
$items = [
new NewsItem([
'title' => $title,
'description' => $description,
'url' => $url,
'image' => $image
])
];
}
return new News($items);
}
/**
* 回复文章消息
* @param string|array $title 标题
* @param string $thumb_media_id 图文消息的封面图片素材id(必须是永久 media_ID)
* @param string $source_url 图文消息的原文地址,即点击“阅读原文”后的URL
* @param string $content 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS
* @param string $author 作者
* @param string $digest 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空
* @param int $show_cover_pic 是否显示封面,0为false,即不显示,1为true,即显示
* @param int $need_open_comment 是否打开评论,0不打开,1打开
* @param int $only_fans_can_comment 是否粉丝才可评论,0所有人可评论,1粉丝才可评论
* @return Article
*/
public static function articleMessage(array|string $title, string $thumb_media_id, string $source_url, string $content = '', string $author = '', string $digest = '', int $show_cover_pic = 0, int $need_open_comment = 0, int $only_fans_can_comment = 1): Article
{
$data = is_array($title) ? $title : compact('title', 'thumb_media_id', 'source_url', 'content', 'author', 'digest', 'show_cover_pic', 'need_open_comment', 'only_fans_can_comment');
return new Article($data);
}
/**
* 回复素材消息
* @param string $mediaId
* @param string|null $type mpnews、 mpvideo、voice、image
* @return Media
*/
public static function materialMessage(string $mediaId, string|null $type = null): Media
{
return new Media($mediaId, $type);
}
/**
* 多客服消息转发
* @param string|null $account
* @return Transfer
*/
public static function transfer(string $account = null): Transfer
{
return new Transfer($account);
}
/**
* 消息模板
* @param string $title
* @param string $description
* @param string $url
* @return TextCard
*/
public static function TextCardMessage(string $title, string $description, string $url): TextCard
{
return new TextCard([
'title' => $title,
'description' => $description,
'url' => $url
]);
}
}

View File

@@ -0,0 +1,698 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use crmeb\services\wechat\client\miniprogram\MediaCheckClient;
use EasyWeChat\Kernel\Exceptions\BadRequestException;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\MiniApp\Application;
use GuzzleHttp\Exception\GuzzleException;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\ResponseInterface;
use crmeb\services\wechat\config\MiniProgramConfig;
use crmeb\services\wechat\client\miniprogram\AuthClient;
use crmeb\services\wechat\client\miniprogram\LiveClient;
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
use crmeb\services\wechat\client\miniprogram\OrderClient;
use crmeb\services\wechat\client\official\MaterialClient;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use crmeb\services\wechat\client\miniprogram\SubscribeClient;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use think\exception\ValidateException;
use think\facade\Log;
/**
* 小程序服务
* Class MiniProgram
* @package crmeb\services\wechat
* @method AuthClient auth() 授权
* @method LiveClient live() 直播
* @method MaterialClient material() 素材
* @method OrderClient order() 订单
* @method SubscribeClient subscribe() 订阅消息
* @method MediaCheckClient media_check() 敏感词
*/
class MiniProgram extends BaseApplication
{
/**
* @var string
*/
protected string $name = 'miniprogram';
/**
* @var MiniProgramConfig
*/
protected MiniProgramConfig $config;
/**
* @var Application
*/
protected $application;
/**
* MiniProgram constructor.
*/
public function __construct()
{
$this->debug = DefaultConfig::value('logger');
}
/**
* 初始化
* @return Application
* @throws InvalidArgumentException
*/
public function application(): Application
{
if (!$this->application) {
$this->application = new Application($this->config->all());
$this->setHttpClient($this->application);
$this->setRequest($this->application);
$this->setLogger($this->application);
$this->setCache($this->application);
}
return $this->application;
}
/**
* @return \think\Response
* @throws InvalidArgumentException
* @throws BadRequestException
* @throws RuntimeException
* @throws \ReflectionException
* @throws \Throwable
* @author 等风来
* @email 136327134@qq.com
* @date 2023/10/7
*/
public static function serve(): \think\Response
{
$make = self::instance();
$server = $make->application()->getServer();
$server->with($make->pushMessageHandler);
$response = $server->serve();
return response($response->getBody());
}
/**
* @return MiniProgram
*/
public static function instance(): static
{
$instance = app()->make(self::class);
$instance->config = app()->make(MiniProgramConfig::class);
return $instance;
}
/**
* 获得用户信息 根据code 获取session_key
* @param string $code
* @return Response|ResponseInterface
*/
public static function getUserInfo(string $code)
{
try {
$response = self::instance()->auth()->session($code);
self::logger('获得用户信息 根据code 获取session_key', compact('code'), $response);
return $response;
} catch (\Throwable $e) {
throw new WechatException($e->getMessage());
}
}
/**
* 解密数据
* @param string $sessionKey
* @param string $iv
* @param string $encryptData
* @return array
*/
public static function decryptData(string $sessionKey, string $iv, string $encryptData)
{
$response = self::instance()->auth()->decryptData($sessionKey, $iv, $encryptData);
self::logger('解密数据', compact('sessionKey', 'iv', 'encryptData'), $response);
return $response;
}
/**
* 获取小程序码:适用于需要的码数量极多,或仅临时使用的业务场景
* @param string $scene
* @param string $path
* @param int $width
* @return Response|string
* @throws TransportExceptionInterface
*/
public static function appCodeUnlimit(string $scene, string $path = '', int $width = 0)
{
$optional = [
'page' => $path,
'width' => $width
];
if (!$optional['page']) {
unset($optional['page']);
}
if (!$optional['width']) {
unset($optional['width']);
}
try {
$response = self::instance()->auth()->getUnlimit($scene, $optional)->getContent();
$res = json_decode($response);
if (isset($res->errcode) && $res->errcode == 40001) {
self::instance()->application()->getAccessToken()->refresh();
$response = self::instance()->auth()->getUnlimit($scene, $optional)->getContent();
}
} catch (\Throwable $e) {
Log::error('获取小程序码错误:' . $e->getMessage());
$response = [];
}
return $response;
}
/**
* 获取短链
* @param string $page
* @param string $query
* @return Response|string|ResponseInterface
* User: liusl
* DateTime: 2024/11/23 下午2:54
*/
public static function generateUrlLink(string $page, string $query)
{
try {
return self::instance()->auth()->generateUrlLink($page, $query);
} catch (\Throwable $e) {
Log::error('获取小程序码错误:' . $e->getMessage());
return '';
}
}
/**
* 发送订阅消息
* @param string $touser
* @param string $templateId
* @param array $data
* @param string $link
* @return Response|ResponseInterface
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws GuzzleException
*/
public static function sendSubscribeTemlate(string $touser, string $templateId, array $data, string $link = '')
{
$response = self::instance()->subscribe()->send([
'template_id' => $templateId,
'touser' => $touser,
'page' => $link,
'data' => $data
]);
self::logger('发送订阅消息', compact('templateId', 'touser', 'link', 'data'), $response);
return $response;
}
/**
* 添加订阅消息模版
* @param string $tid
* @param array $kidList
* @param string $sceneDesc
* @return mixed
*/
public static function addSubscribeTemplate(string $tid, array $kidList, string $sceneDesc = '')
{
try {
$res = self::instance()->subscribe()->addTemplate($tid, $kidList, $sceneDesc);
self::logger('添加订阅消息模版', compact('tid', 'kidList', 'sceneDesc'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['priTmplId'])) {
return $res['priTmplId'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException($e);
}
}
/**
* 删除订阅消息
* @param string $templateId
* @return Response|ResponseInterface
*/
public static function delSubscribeTemplate(string $templateId)
{
try {
$response = self::instance()->subscribe()->deleteTemplate($templateId);
self::logger('删除订阅消息', compact('templateId'), $response);
return $response;
} catch (\Throwable $e) {
throw new WechatException($e->getMessage());
}
}
/**
* 获取模版标题的关键词列表
* @param string $tid
* @return mixed
*/
public static function getSubscribeTemplateKeyWords(string $tid)
{
try {
$res = self::instance()->subscribe()->getTemplateKeywords($tid);
self::logger('获取模版标题的关键词列表', compact('tid'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['data'])) {
return $res['data'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException($e);
}
}
/**
* 获取直播列表
* @param int $page
* @param int $limit
* @return array
*/
public static function getLiveInfo(int $page = 1, int $limit = 10)
{
try {
$res = self::instance()->live()->getRooms($page, $limit);
// self::logger('获取直播列表', compact('page', 'limit'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['room_info']) && $res['room_info']) {
return $res['room_info'];
} else {
return [];
}
} catch (\Throwable $e) {
return [];
}
}
/**
* 获取直播回放
* @param int $room_id
* @param int $page
* @param int $limit
* @return mixed
*/
public static function getLivePlayback(int $room_id, int $page = 1, int $limit = 10)
{
try {
$res = self::instance()->live()->getPlaybacks($room_id, $page, $limit);
self::logger('获取直播回放', compact('room_id', 'page', 'limit'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['live_replay'])) {
return $res['live_replay'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 创建直播间
* @param array $data
* @return mixed
*/
public static function createLiveRoom(array $data)
{
try {
$res = self::instance()->live()->createRoom($data);
self::logger('创建直播间', compact('data'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['roomId'])) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 直播间添加商品
* @param int $roomId
* @param $ids
* @return bool
*/
public static function roomAddGoods(int $roomId, $ids)
{
try {
$res = self::instance()->live()->roomAddGoods($roomId, $ids);
self::logger('直播间添加商品', compact('roomId', 'ids'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0) {
return true;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 获取商品列表
* @param int $status
* @param int $page
* @param int $limit
* @return mixed
*/
public static function getGoodsList(int $status = 2, int $page = 1, int $limit = 10)
{
try {
$res = self::instance()->live()->getGoodsList($status, $page, $limit);
self::logger('获取商品列表', compact('status', 'page', 'limit'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['goods'])) {
return $res['goods'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 获取商品详情
* @param $goods_ids
* @return mixed
*/
public static function getGooodsInfo($goods_ids)
{
try {
$res = self::instance()->live()->getGooodsInfo($goods_ids);
self::logger('获取商品详情', compact('goods_ids'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['goods'])) {
return $res['goods'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 添加商品
* @param string $coverImgUrl
* @param string $name
* @param int $priceType
* @param string $url
* @param $price
* @param string $price2
* @return mixed
*/
public static function addGoods(string $coverImgUrl, string $name, int $priceType, string $url, $price, $price2 = '')
{
try {
$res = self::instance()->live()->addGoods($coverImgUrl, $name, $priceType, $url, $price, $price2);
self::logger('添加商品', compact('coverImgUrl', 'name', 'priceType', 'url', 'price', 'price2'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['goodsId'])) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 商品撤回审核
* @param int $goodsId
* @param $auditId
* @return bool
*/
public static function resetauditGoods(int $goodsId, $auditId)
{
try {
$res = self::instance()->live()->resetauditGoods($goodsId, $auditId);
self::logger('商品撤回审核', compact('goodsId', 'auditId'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0) {
return true;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 商品重新提交审核
* @param int $goodsId
* @return mixed
*/
public static function auditGoods(int $goodsId)
{
try {
$res = self::instance()->live()->auditGoods($goodsId);
self::logger('商品重新提交审核', compact('goodsId'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['auditId'])) {
return $res['auditId'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 删除商品
* @param int $goodsId
* @return bool
*/
public static function deleteGoods(int $goodsId)
{
try {
$res = self::instance()->live()->deleteGoods($goodsId);
self::logger('删除商品', compact('goodsId'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0) {
return true;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 更新商品
* @param int $goodsId
* @param string $coverImgUrl
* @param string $name
* @param int $priceType
* @param string $url
* @param $price
* @param string $price2
* @return bool
*/
public static function updateGoods(int $goodsId, string $coverImgUrl, string $name, int $priceType, string $url, $price, $price2 = '')
{
try {
$res = self::instance()->live()->updateGoods($goodsId, $coverImgUrl, $name, $priceType, $url, $price, $price2);
self::logger('更新商品', compact('goodsId', 'coverImgUrl', 'name', 'priceType', 'url', 'price', 'price2'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0) {
return true;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 获取成员列表
* @param int $role
* @param int $page
* @param int $limit
* @param string $keyword
* @return mixed
*/
public static function getRoleList(int $role = 2, int $page = 0, int $limit = 30, string $keyword = '')
{
try {
$res = self::instance()->live()->getRoleList($role, $page, $limit, $keyword);
self::logger('获取成员列表', compact('role', 'page', 'limit', 'keyword'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['list'])) {
return $res['list'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Throwable $e) {
throw new WechatException(ErrorMessage::getValidMessgae($e));
}
}
/**
* 小程序临时素材上传
* @param string $path
* @param string $type
* @return WechatResponse
* @throws TransportExceptionInterface
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
*/
public static function temporaryUpload(string $path, string $type = 'image')
{
$response = self::instance()->material()->upload($type, $path);
self::logger('小程序-临时素材上传', compact('path', 'type'), $response);
return new WechatResponse($response);
}
/**
* 上传订单
* @param string $out_trade_no 订单号(商城订单好)
* @param int $logistics_type 物流模式,发货方式枚举值:1、实体物流配送采用快递公司进行实体物流配送形式 2、同城配送 3、虚拟商品,虚拟商品,例如话费充值,点卡等,无实体配送形式 4、用户自提
* @param array $shipping_list 物流信息列表,发货物流单列表,支持统一发货(单个物流单)和分拆发货(多个物流单)两种模式,多重性: [1, 10]
* @param string $payer_openid 支付者,支付者信息
* @param string $path
* @param int $delivery_mode 发货模式,发货模式枚举值:1、UNIFIED_DELIVERY(统一发货)2、SPLIT_DELIVERY(分拆发货) 示例值: UNIFIED_DELIVERY
* @param bool $is_all_delivered 分拆发货模式时必填,用于标识分拆发货模式下是否已全部发货完成,只有全部发货完成的情况下才会向用户推送发货完成通知。示例值: true/false
* @return array
* @throws TransportExceptionInterface
*/
public static function shippingByTradeNo(string $out_trade_no, int $logistics_type, array $shipping_list, string $payer_openid, string $path, int $delivery_mode = 1, bool $is_all_delivered = true)
{
return self::instance()->order()->shippingByTradeNo($out_trade_no, $logistics_type, $shipping_list, $payer_openid, $path, $delivery_mode, $is_all_delivered);
}
/**
* 合单
* @param string $out_trade_no
* @param int $logistics_type
* @param array $sub_orders
* @param string $payer_openid
* @param int $delivery_mode
* @param bool $is_all_delivered
* @return array
* @throws TransportExceptionInterface
*/
public static function combinedShippingByTradeNo(string $out_trade_no, int $logistics_type, array $sub_orders, string $payer_openid, int $delivery_mode = 2, bool $is_all_delivered = false)
{
return self::instance()->order()->combinedShippingByTradeNo($out_trade_no, $logistics_type, $sub_orders, $payer_openid, $delivery_mode, $is_all_delivered);
}
/**
* 签收通知
* @param string $merchant_trade_no
* @param string $received_time
* @return array
* @throws TransportExceptionInterface
*/
public static function notifyConfirmByTradeNo(string $merchant_trade_no, string $received_time)
{
return self::instance()->order()->notifyConfirmByTradeNo($merchant_trade_no, $received_time);
}
/**
* 判断是否开通
* @return bool
* @throws HttpException
* @date 2023/05/17
* @author yyw
*/
public static function isManaged()
{
return self::instance()->order()->checkManaged();
}
/**
* 设置小修跳转路径
* @param $path
* @return array
* @throws HttpException|TransportExceptionInterface
*/
public static function setMesJumpPathAndCheck($path)
{
return self::instance()->order()->setMesJumpPathAndCheck($path);
}
/**
* 敏感词验证
* @param string $content
* @param int $scene
* @param string $openId
* @return true
* User: liusl
* DateTime: 2024/11/26 下午4:29
*/
public static function msgSecCheck(string $content)
{
// try{
//scene 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志)
$res = self::instance()->media_check()->msgSecCheck($content);
var_dump($res);
if($res->result['label'] == 100) {
return true;
} else {
throw new ValidateException('内容包含:【'.$res->result['label'].'】无法发布');
}
// } catch (\Exception $e) {
// Log::error('敏感词检查:' . $e->getMessage());
// throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
// }
}
}

View File

@@ -0,0 +1,713 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use crmeb\services\wechat\client\official\CardClient;
use crmeb\services\wechat\client\official\MaterialClient;
use crmeb\services\wechat\client\official\MediaClient;
use crmeb\services\wechat\client\official\QrCodeClient;
use crmeb\services\wechat\client\official\StaffClient;
use crmeb\services\wechat\client\official\TemplateClient;
use crmeb\services\wechat\config\OfficialAccountConfig;
use crmeb\services\wechat\config\OpenAppConfig;
use crmeb\services\wechat\config\OpenWebConfig;
use crmeb\services\wechat\client\official\UserClient;
use crmeb\services\wechat\message\MessageInterface;
use EasyWeChat\Kernel\Exceptions\BadRequestException;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use EasyWeChat\OfficialAccount\Application;
use ReflectionException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use EasyWeChat\Kernel\HttpClient\Response;
use Throwable;
/**
* 公众号服务
* Class OfficialAccount
* @package crmeb\services\wechat
* @method UserClient user() 用户
* @method CardClient card() 卡卷
* @method TemplateClient template() 模板消息
* @method MaterialClient material() 永久素材管理
* @method MediaClient media() 临时素材管理
* @method StaffClient staff() 消息
* @method QrCodeClient qrCode() 二维码
*/
class OfficialAccount extends BaseApplication
{
/**
* @var string
*/
protected string $name = 'official';
/**
* @var array
*/
protected array $application;
/**
* OfficialAccount constructor.
*/
public function __construct()
{
$this->debug = DefaultConfig::value('logger');
}
/**
* 初始化
* @return Application
* @throws InvalidArgumentException
*/
public function application(): Application
{
$request = request();
$config = match ($accessEnd = $this->getAuthAccessEnd($request)) {
self::APP => app()->make(OpenAppConfig::class)->all(),
self::PC => app()->make(OpenWebConfig::class)->all(),
default => app()->make(OfficialAccountConfig::class)->all(),
};
if (!isset($this->application[$accessEnd])) {
$this->application[$accessEnd] = new Application($config);
$this->setHttpClient($this->application[$accessEnd]);
$this->setRequest($this->application[$accessEnd]);
$this->setLogger($this->application[$accessEnd]);
$this->setCache($this->application[$accessEnd]);
}
return $this->application[$accessEnd];
}
/**
* 服务端
* @return \think\Response
* @throws BadRequestException
* @throws InvalidArgumentException
* @throws ReflectionException
* @throws RuntimeException
* @throws Throwable
*/
public static function serve(): \think\Response
{
$make = self::instance();
$server = $make->application()->getServer();
$server->with($make->pushMessageHandler);
$response = $server->serve();
return response($response->getBody());
}
/**
* @return OfficialAccount
*/
public static function instance(): static
{
return app()->make(self::class);
}
/**
* 获取js的SDK
* @param string $url
* @return array
*/
public static function jsSdk(string $url = ''): array
{
$apiList = ['openAddress', 'updateTimelineShareData', 'updateAppMessageShareData',
'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ',
'onMenuShareWeibo', 'onMenuShareQZone', 'startRecord', 'stopRecord',
'onVoiceRecordEnd', 'playVoice', 'pauseVoice', 'stopVoice',
'onVoicePlayEnd', 'uploadVoice', 'downloadVoice', 'chooseImage',
'previewImage', 'uploadImage', 'downloadImage', 'translateVoice',
'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu',
'showOptionMenu', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem',
'showAllNonBaseMenuItem', 'closeWindow', 'scanQRCode', 'chooseWXPay',
'openProductSpecificView', 'addCard', 'chooseCard', 'openCard','requestMerchantTransfer'];
try {
return self::retryWithTokenRefresh(function() use ($url, $apiList) {
return self::instance()->application()->getUtils()->buildJsSdkConfig(
url: $url,
jsApiList: $apiList,
openTagList: [],
debug: false
);
});
} catch (\Throwable $e) {
self::error($e);
return [];
}
}
/**
* 获取微信用户信息
* @param $openid
* @return array|Response|mixed|ResponseInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public static function getUserInfo($openid)
{
try {
$userInfo = self::retryWithTokenRefresh(function() use ($openid) {
$userService = self::instance()->user();
if (is_array($openid)) {
$res = $userService->select($openid);
if (isset($res['user_info_list'])) {
return $res['user_info_list'];
} else {
throw new WechatException($res['errmsg'] ?? '获取微信粉丝信息失败');
}
} else {
$userInfo = $userService->get($openid);
return is_object($userInfo) ? $userInfo->toArray() : $userInfo;
}
});
} catch (Throwable $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
self::logger('获取微信用户信息', compact('openid'), $userInfo);
return $userInfo;
}
/**
* 获取会员卡列表
* @param int $offset
* @param int $count
* @param string $statusList
* @return mixed
* @throws TransportExceptionInterface
*/
public static function getCardList(int $offset = 0, int $count = 10, string $statusList = 'CARD_STATUS_VERIFY_OK')
{
try {
$res = self::instance()->card()->list($offset, $count, $statusList);
self::logger('获取会员卡列表', compact('offset', 'count', 'statusList'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['card_id_list'])) {
return $res['card_id_list'];
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 获取卡券颜色
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function getCardColors()
{
try {
$response = self::instance()->card()->colors();
self::logger('获取卡券颜色', [], $response);
return $response;
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 创建卡券
* @param string $cardType
* @param array $baseInfo
* @param array $especial
* @param array $advancedInfo
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function createCard(string $cardType, array $baseInfo, array $especial = [], array $advancedInfo = []): ResponseInterface|Response
{
try {
$res = self::instance()->card()->create($cardType, array_merge(['base_info' => $baseInfo, 'advanced_info' => $advancedInfo], $especial));
self::logger('创建卡券', compact('cardType', 'baseInfo', 'especial', 'advancedInfo'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['card_id'])) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* @param int $cardId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public static function getCard(int $cardId): ResponseInterface|Response
{
try {
$res = self::retryWithTokenRefresh(function() use ($cardId) {
return self::instance()->card()->get($cardId);
});
self::logger('获取卡券信息', compact('cardId'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 修改卡券
* @param string $cardId
* @param string $type
* @param array $baseInfo
* @param array $especial
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function updateCard(string $cardId, string $type, array $baseInfo = [], array $especial = []): ResponseInterface|Response
{
try {
$res = self::retryWithTokenRefresh(function() use ($cardId, $type, $baseInfo, $especial) {
return self::instance()->card()->update($cardId, $type, array_merge(['base_info' => $baseInfo], $especial));
});
self::logger('修改卡券', compact('cardId', 'type', 'baseInfo', 'especial'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 获取领卡券二维码
* @param string $card_id 卡券ID
* @param string $outer_id 生成二维码标识参数
* @param string $code 自动移code
* @param int $expire_time
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function getCardQRCode(string $card_id, string $outer_id, string $code = '', int $expire_time = 1800): ResponseInterface|Response
{
$data = [
'action_name' => 'QR_CARD',
'expire_seconds' => $expire_time,
'action_info' => [
'card' => [
'card_id' => $card_id,
'is_unique_code' => false,
'outer_id' => $outer_id
]
]
];
if ($code) $data['action_info']['card']['code'] = $code;
try {
$res = self::instance()->card()->createQrCode($data);
self::logger('获取领卡券二维码', compact('data'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['url'])) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 设置会员卡激活字段
* @param string $cardId
* @param array $requiredForm
* @param array $optionalForm
* @return mixed
* @throws TransportExceptionInterface
*/
public static function cardActivateUserForm(string $cardId, array $requiredForm = [], array $optionalForm = []): mixed
{
try {
$res = self::instance()->card()->setActivationForm($cardId, array_merge($requiredForm, $optionalForm));
self::logger('设置会员卡激活字段', compact('cardId', 'requiredForm', 'optionalForm'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 会员卡激活
* @param string $card_id
* @param string $code
* @param string $membership_number
* @return mixed
* @throws TransportExceptionInterface
*/
public static function cardActivate(string $card_id, string $code, string $membership_number = ''): mixed
{
$info = [
'membership_number' => $membership_number ? $membership_number : $code, //会员卡编号,由开发者填入,作为序列号显示在用户的卡包里。可与Code码保持等值。
'code' => $code, //创建会员卡时获取的初始code。
'activate_begin_time' => '', //激活后的有效起始时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式
'activate_end_time' => '', //激活后的有效截至时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。
'init_bonus' => '0', //初始积分,不填为0。
'init_balance' => '0', //初始余额,不填为0。
];
try {
$res = self::instance()->card()->activate($info);
self::logger('会员卡激活', compact('info'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['url'])) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 获取会员信息
* @param string $cardId
* @param string $code
* @return mixed
* @throws TransportExceptionInterface
*/
public static function getMemberCardUser(string $cardId, string $code): mixed
{
try {
$res = self::instance()->card()->getUser($cardId, $code);
self::logger('获取会员信息', compact('cardId', 'code'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['user_info'])) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 更新会员信息
* @param array $data
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function updateMemberCardUser(array $data): ResponseInterface|Response
{
try {
$res = self::instance()->card()->updateUser($data);
self::logger('更新会员信息', compact('data'), $res);
if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['user_info'])) {
return $res;
} else {
throw new WechatException($res['errmsg']);
}
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 设置模版消息行业
* @param int $industryOne
* @param int $industryTwo
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public static function setIndustry(int $industryOne, int $industryTwo): ResponseInterface|Response
{
$response = self::instance()->template()->setIndustry($industryOne, $industryTwo);
self::logger('设置模版消息行业', compact('industryOne', 'industryTwo'), $response);
return $response;
}
/**
* 获得添加模版ID
* @param $templateIdShort
* @param $keywordList
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function addTemplateId($templateIdShort, $keywordList): ResponseInterface|Response
{
try {
$response = self::instance()->template()->addTemplate($templateIdShort, $keywordList);
self::logger('获得添加模版ID', compact('templateIdShort'), $response);
return $response;
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 获取模板列表
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function getPrivateTemplates(): ResponseInterface|Response
{
try {
$response = self::instance()->template()->getPrivateTemplates();
self::logger('获取模板列表', [], $response);
return $response;
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 根据模版ID删除模版
* @param string $templateId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function deleleTemplate(string $templateId): ResponseInterface|Response
{
try {
return self::instance()->template()->deletePrivateTemplate($templateId);
} catch (\Exception $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 获取行业
* @return Response|ResponseInterface
*/
public static function getIndustry(): ResponseInterface|Response
{
try {
$response = self::instance()->template()->getIndustry();
self::logger('获取行业', [], $response);
return $response;
} catch (Throwable $e) {
throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
}
}
/**
* 发送模板消息
* @param string $openid
* @param string $templateId
* @param array $data
* @param string|null $url
* @param string|null $defaultColor
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public static function sendTemplate(string $openid, string $templateId, array $data, string $url = null, string $defaultColor = null): ResponseInterface|Response
{
$response = self::instance()->template()->send([
'touser' => $openid,
'template_id' => $templateId,
'data' => $data,
'url' => $url
]);
self::logger('发送模板消息', compact('openid', 'templateId', 'data', 'url'), $response);
return $response;
}
/**
* 静默授权-使用code获取用户授权信息
* @param string|null $code
* @return array
*/
public static function tokenFromCode(string $code = null): array
{
$code = $code ?: request()->param('code');
if (!$code) {
throw new WechatException('无效CODE');
}
try {
$response = self::instance()->application()->getOauth()->userFromCode($code);
self::logger('静默授权-使用code获取用户授权信息', compact('code'), $response);
return $response->getTokenResponse();
} catch (Throwable $e) {
throw new WechatException('授权失败' . $e->getMessage() . 'line' . $e->getLine());
}
}
/**
* 使用code获取用户授权信息
* @param string|null $code
* @return array
*/
public static function userFromCode(string $code = null): array
{
$code = $code ?: request()->param('code');
if (!$code) {
throw new WechatException('无效CODE');
}
try {
$response = self::instance()->application()->getOauth()->userFromCode($code);
self::logger('使用code获取用户授权信息', compact('code'), $response);
return $response->getRaw();
} catch (Throwable $e) {
throw new WechatException('授权失败' . $e->getMessage() . 'line' . $e->getLine());
}
}
/**
* 临时素材上传
* @param string $path
* @return WechatResponse
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
public static function uploadImage(string $path): WechatResponse
{
$response = self::instance()->media()->uploadImage($path);
self::logger('素材管理-上传附件', compact('path'), $response);
return new WechatResponse($response);
}
/**
* 永久素材上传
* @param string $path
* @param string $type
* @return WechatResponse
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
*/
public static function temporaryUpload(string $path, string $type = 'image'): WechatResponse
{
$response = self::retryWithTokenRefresh(function() use ($path, $type) {
return self::instance()->material()->upload($type, $path);
});
self::logger('临时素材上传', compact('path', 'type'), $response);
return new WechatResponse($response);
}
/**
* 消息回执
* @param MessageInterface|string $message
* @param string $to
* @param string $account
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public static function staffSend(MessageInterface|string $message, string $to, string $account = '')
{
return self::retryWithTokenRefresh(function() use ($message, $to, $account) {
return self::instance()->staff()->send($message, $to, $account);
});
}
/**
* 通用的access_token刷新重试方法
* @param callable $callback 需要重试的回调函数
* @param int $maxRetries 最大重试次数
* @return mixed
* @throws WechatException
*/
protected static function retryWithTokenRefresh(callable $callback, int $maxRetries = 1)
{
$attempts = 0;
while ($attempts <= $maxRetries) {
try {
return $callback();
} catch (Throwable $e) {
$attempts++;
// 检查是否是access_token过期错误
$errorMessage = $e->getMessage();
$isTokenError = strpos($errorMessage, 'invalid credential') !== false ||
strpos($errorMessage, 'access_token is invalid') !== false ||
(method_exists($e, 'getCode') && $e->getCode() == 40001);
// 如果是token错误且还有重试机会,则刷新token并重试
if ($isTokenError && $attempts <= $maxRetries) {
try {
self::instance()->application()->getAccessToken()->refresh();
continue; // 继续下一次循环重试
} catch (Throwable $refreshException) {
// 刷新token失败,抛出原始异常
throw $e;
}
}
// 不是token错误或已达到最大重试次数,抛出异常
throw $e;
}
}
throw new WechatException('重试次数已达上限');
}
}

View File

@@ -0,0 +1,834 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use app\services\kefu\UserServices;
use app\services\wechat\WechatUserServices;
use crmeb\services\wechat\util\XML;
use Symfony\Contracts\HttpClient\ResponseInterface;
use think\exception\ValidateException;
use Throwable;
use think\Response;
use ReflectionException;
use think\facade\Event;
use EasyWeChat\Pay\Application;
use crmeb\exceptions\PayException;
use crmeb\services\wechat\v3pay\PayClient;
use crmeb\services\wechat\config\OpenAppConfig;
use crmeb\services\wechat\config\OpenWebConfig;
use crmeb\services\wechat\config\PaymentConfig;
use crmeb\services\wechat\config\V3PaymentConfig;
use crmeb\services\wechat\client\pay\OrderClient;
use EasyWeChat\Kernel\Exceptions\RuntimeException;
use crmeb\services\wechat\config\MiniProgramConfig;
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* 微信支付
* Class Payment
* @package crmeb\services\wechat
*/
class Payment extends BaseApplication
{
const BASE_PAY_URL = 'https://api.mch.weixin.qq.com';
/**
* @var PaymentConfig
*/
protected PaymentConfig $config;
/**
* @var
*/
protected V3PaymentConfig $v3Config;
/**
* @var PayClient
*/
protected PayClient $payClient;
/**
* 是否v3支付
* @var bool
*/
public bool $isV3PAy = true;
/**
* @var array
*/
protected array $application = [];
/**
* Payment constructor.
* @param PaymentConfig $config
* @param V3PaymentConfig $v3Config
* @param PayClient $payClient
*/
public function __construct(PaymentConfig $config, V3PaymentConfig $v3Config, PayClient $payClient)
{
$this->config = $config;
$this->v3Config = $v3Config;
$this->payClient = $payClient;
$this->isV3PAy = $this->v3Config->isV3PAy;
$this->debug = !!DefaultConfig::value('logger');
}
/**
* @return Payment
*/
public static function instance(): static
{
return app()->make(static::class);
}
/**
* @return Application
* @throws InvalidArgumentException
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/11
*/
public function application(): Application
{
$request = request();
$config = $this->config->all();
switch ($accessEnd = $this->getAuthAccessEnd($request)) {
case self::APP:
/** @var OpenAppConfig $make */
$make = app()->make(OpenAppConfig::class);
$config['app_id'] = $make->appId;
$config['notify_url'] = trim($make->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('app.notifyUrl');
break;
case self::PC:
/** @var OpenWebConfig $make */
$make = app()->make(OpenWebConfig::class);
$config['app_id'] = $make->appId;
break;
case self::MINI:
/** @var MiniProgramConfig $make */
$make = app()->make(MiniProgramConfig::class);
$config['app_id'] = $make->appId;
$config['notify_url'] = trim($make->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('mini.notifyUrl');
break;
}
if (!isset($this->application[$accessEnd])) {
$this->application[$accessEnd] = new Application($config);
$this->setHttpClient($this->application[$accessEnd], self::BASE_PAY_URL);
$this->setRequest($this->application[$accessEnd]);
}
return $this->application[$accessEnd];
}
/**
* 发起订单支付接口入口
* @return OrderClient
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @author 等风来
* @email 136327134@qq.com
* @date 2023/10/10
*/
public function order()
{
$api = $this->application()->getClient();
$config = $this->application()->getConfig();
return new OrderClient($api, $config);
}
/**
* 付款码支付
* @param string $authCode
* @param string $outTradeNo
* @param string $totalFee
* @param string $attach
* @param string $body
* @param string $detail
* @return array
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws TransportExceptionInterface
*/
public static function microPay(string $authCode, string $outTradeNo, string $totalFee, string $attach, string $body, string $detail = '')
{
$totalFee = bcmul($totalFee, 100, 0);
$response = self::instance()->order()->pay([
'auth_code' => $authCode,
'out_trade_no' => $outTradeNo,
'total_fee' => (int)$totalFee,
'attach' => $attach,
'body' => $body,
'detail' => $detail
]);
$response = $response->toArray();
self::logger('付款码支付', compact('authCode', 'outTradeNo', 'totalFee', 'attach', 'body', 'detail'), $response);
//下单成功
if ($response['return_code'] === 'SUCCESS') {
//扫码付款直接支付成功
if ($response['result_code'] === 'SUCCESS' && $response['trade_type'] === 'MICROPAY') {
return [
'paid' => 1,
'message' => '支付成功',
'payInfo' => $response,
];
} else {
return [
'paid' => 0,
'message' => $response['err_code_des'],
'payInfo' => $response
];
}
} else {
throw new PayException($response['return_msg']);
}
}
/**
* 撤销订单
* @param string $outTradeNo
* @return bool
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws TransportExceptionInterface
*/
public static function reverseOrder(string $outTradeNo): bool
{
$response = self::instance()->order()->reverseByOutTradeNumber($outTradeNo);
$response = $response->toArray();
self::logger('撤销订单', compact('outTradeNo'), $response);
if ($response['return_code'] === 'SUCCESS') {
return true;
} else {
throw new PayException($response['return_msg']);
}
}
/**
* 查询订单支付状态
* @param string $outTradeNo
* @return array
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws TransportExceptionInterface
*/
public static function queryOrder(string $outTradeNo)
{
$response = self::instance()->order()->queryByOutTradeNumber($outTradeNo);
$response = $response->toArray();
self::logger('查询订单支付状态', compact('outTradeNo'), $response);
if ($response['return_code'] === 'SUCCESS') {
if ($response['result_code'] === 'SUCCESS') {
return [
'paid' => 1,
'out_trade_no' => $outTradeNo,
'payInfo' => $response
];
} else {
return [
'paid' => 0,
'out_trade_no' => $outTradeNo,
'payInfo' => $response
];
}
} else {
throw new PayException($response['return_msg']);
}
}
/**
* 企业付款到零钱
* @param string $openid openid
* @param string $orderId 订单号
* @param string $amount 金额
* @param string $desc 说明
* @param string $type 类型
* @return bool
* @throws InvalidArgumentException
* @throws TransportExceptionInterface
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidConfigException
*/
public static function merchantPay(string $channel_type, int $uid, string $orderId, string $amount, string $desc, string $user_name = '', array $transferDetailList = [])
{
/** @var WechatUserServices $wechatServices */
$wechatServices = app()->make(WechatUserServices::class);
$typeMap = [
'routine' => Payment::MINI,
'wechat' => Payment::WEB,
'app' => Payment::APP
];
$channelType = $channel_type;
$openid = '';
$type = null;
if (isset($typeMap[$channelType])) {
$openid = $wechatServices->getWechatOpenid($uid, $channelType);
$type = $typeMap[$channelType];
}
if (!$openid) {
throw new PayException('该用户暂不支持企业付款到零钱,请手动转账');
}
$application = self::instance()->setAccessEnd($type)->application();
$config = $application->getConfig();
if (empty($config['certificate'])) {
throw new PayException('企业付款到零钱需要支付cert证书,检测到您没有上传!');
}
if (empty($config['private_key'])) {
throw new PayException('企业付款到零钱需要支付key证书,检测到您没有上传!');
}
if (self::instance()->isV3PAy) {
if (sys_config('v3_pay_public_key') != '') {
$transfer_scene_id = sys_config('pay_weixin_scene_id');
if (!$transfer_scene_id) {
throw new PayException('请配置微信v3新支付场景ID!');
}
$user_name = $user_name ?: app()->make(UserServices::class)->value(['uid' => $uid], 'nickname');
//v3新支付
$res = self::instance()->payClient->setType($type)->transferBills(
outBatchNo: $orderId,
amount: $amount,
openid: $openid,
userName: $user_name,
remark: $desc,
perception: $desc,
transferDetailList: $transferDetailList,
transfer_scene_id: $transfer_scene_id
);
self::logger('商家转账到零钱', compact('orderId', 'amount', 'openid', 'desc', 'user_name', 'transferDetailList', 'transfer_scene_id'), $res);
} else {
//v3支付使用发起商家转账API
$res = self::instance()->payClient->setType($type)->batches(
outBatchNo: $orderId,
amount: $amount,
batchName: $desc,
remark: $desc,
transferDetailList: [
[
'out_detail_no' => $orderId,
'transfer_amount' => $amount,
'transfer_remark' => $desc,
'openid' => $openid
]
]
);
}
return $res;
} else {
$merchantPayData = [
'partner_trade_no' => $orderId, //随机字符串作为订单号,跟红包和支付一个概念。
'openid' => $openid, //收款人的openid
'check_name' => 'NO_CHECK', //文档中有三种校验实名的方法 NO_CHECK OPTION_CHECK FORCE_CHECK
'amount' => (int)bcmul($amount, '100', 0), //单位为分
'desc' => $desc,
'spbill_create_ip' => request()->ip(), //发起交易的IP地址
];
$result = self::instance()->order()->toBalance($merchantPayData);
$result = $result->toArray();
self::logger('企业付款到零钱', compact('merchantPayData'), $result);
if ($result['return_code'] == 'SUCCESS' && $result['result_code'] != 'FAIL') {
return true;
} else {
throw new PayException(($result['return_msg'] ?? '支付失败') . ':' . ($result['err_code_des'] ?? '发起企业支付到零钱失败'));
}
}
}
/**
* 生成支付订单对象
* @param $openid
* @param $out_trade_no
* @param $total_fee
* @param $attach
* @param $body
* @param string $detail
* @param string $trade_type
* @param array $options
* @return \EasyWeChat\Kernel\HttpClient\Response
* @throws TransportExceptionInterface
*/
public static function paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', array $options = [])
{
$total_fee = bcmul($total_fee, 100, 0);
$order = array_merge(compact('out_trade_no', 'total_fee', 'attach', 'body', 'detail', 'trade_type'), $options);
if (!is_null($openid)) $order['openid'] = $openid;
if ($order['detail'] == '') unset($order['detail']);
$result = self::instance()->order()->unify($order);
$result = $result->toArray();
self::logger('生成支付订单对象', compact('order'), $result);
if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') {
return $result;
} else {
if ($result['return_code'] == 'FAIL') {
throw new PayException('微信支付错误返回:' . $result['return_msg']);
} else if (isset($result['err_code'])) {
throw new PayException('微信支付错误返回:' . $result['err_code_des']);
} else {
throw new PayException('没有获取微信支付的预支付ID,请重新发起支付!');
}
}
}
/**
* 生成支付订单对象(小程序商户号支付时)
* @param $openid
* @param $out_trade_no
* @param $total_fee
* @param $attach
* @param $body
* @param string $detail
* @param string $trade_type
* @param array $options
* @return \EasyWeChat\Kernel\HttpClient\Response|ResponseInterface
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws TransportExceptionInterface
*/
public static function paymentMiniOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', array $options = [])
{
$total_fee = bcmul($total_fee, 100, 0);
$order = array_merge(compact('out_trade_no', 'total_fee', 'attach', 'body', 'detail', 'trade_type'), $options);
if (!is_null($openid)) $order['openid'] = $openid;
if ($order['detail'] == '') unset($order['detail']);
$order['spbill_create_ip'] = request()->ip();
$result = self::instance()->order()->createorder($order);
$result = $result->toArray();
self::logger('生成支付订单对象', compact('order'), $result);
if ($result['errcode'] == '0') {
return $result;
} else {
throw new PayException('微信支付错误返回:' . $result['errmsg']);
}
}
/**
* 获得jsSdk支付参数
* @param $openid
* @param $out_trade_no
* @param $total_fee
* @param $attach
* @param $body
* @param string $detail
* @param string $trade_type
* @param array $options
* @return array
* @throws InvalidArgumentException
* @throws InvalidConfigException|TransportExceptionInterface
*/
public static function jsPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = [])
{
$pay = self::instance();
$paymentPrepare = self::paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options);
return $pay->application()
->getUtils()
->buildSdkConfig($paymentPrepare['prepay_id'], $pay->application()->getConfig()->get('app_id'), 'MD5');
}
/**
* 获得jsSdk支付参数(小程序商户号支付时)
* @param $openid
* @param $out_trade_no
* @param $total_fee
* @param $attach
* @param $body
* @param string $detail
* @param string $trade_type
* @param array $options
* @return array
* @throws TransportExceptionInterface
*/
public static function miniPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = [])
{
$paymentPrepare = self::paymentMiniOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options);
$paymentPrepare['payment_params']['timestamp'] = $paymentPrepare['payment_params']['timeStamp'];
return $paymentPrepare['payment_params'] ?? [];
}
/**
* 获得APP付参数
* @param $openid
* @param $out_trade_no
* @param $total_fee
* @param $attach
* @param $body
* @param string $detail
* @param string $trade_type
* @param array $options
* @return array|string
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws TransportExceptionInterface
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public static function appPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'APP', $options = [])
{
if (self::instance()->isV3PAy) {
return self::instance()->payClient->appPay($out_trade_no, $total_fee, $body, $attach);
} else {
$paymentPrepare = self::paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options);
return self::instance()->application()->getUtils()->buildAppConfig($paymentPrepare['prepay_id'], self::instance()->application()->getConfig()->get('app_id'));
}
}
/**
* v3 jspay 支付
* @return PayClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/10/10
*/
public function payClient()
{
return $this->payClient;
}
/**
* 获得native支付参数
* @param $openid
* @param $out_trade_no
* @param $total_fee
* @param $attach
* @param $body
* @param string $detail
* @param string $trade_type
* @param array $options
* @return array
* @throws TransportExceptionInterface
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public static function nativePay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'NATIVE', $options = [])
{
$instance = self::instance();
if ($instance->isV3PAy) {
$data = $instance->payClient->nativePay($out_trade_no, $total_fee, $body, $attach);
$res['code_url'] = $data['code_url'];
$res['invalid'] = time() + 60;
$res['logo'] = [];
return $res;
}
$data = $instance->setAccessEnd(self::WEB)->paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options);
if ($data) {
$res['code_url'] = $data['code_url'];
$res['invalid'] = time() + 60;
$res['logo'] = [];
} else $res = [];
return $res;
}
/**
* 使用商户订单号退款
* @param $orderNo
* @param $refundNo
* @param $totalFee
* @param null $refundFee
* @param null $opUserId
* @param string $refundReason
* @param string $type
* @param string $refundAccount
* @return \EasyWeChat\Kernel\HttpClient\Response
* @throws TransportExceptionInterface
*/
public function refund($orderNo, $refundNo, $totalFee, $refundFee = null, $opUserId = null, string $refundReason = '', string $type = 'out_trade_no', string $refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS')
{
$totalFee = floatval($totalFee);
$refundFee = floatval($refundFee);
if ($type == 'out_trade_no') {
$result = $this->order()->byOutTradeNumber($orderNo, $refundNo, $totalFee, $refundFee, [
'refund_account' => $refundAccount,
'notify_url' => $this->config->refundUrl,
'refund_desc' => $refundReason
]);
} else {
$result = $this->order()->byTransactionId($orderNo, $refundNo, $totalFee, $refundFee, [
'refund_account' => $refundAccount,
'notify_url' => $this->config->refundUrl,
'refund_desc' => $refundReason
]);
}
self::logger('使用商户订单号退款', compact('orderNo', 'refundNo', 'totalFee', 'refundFee', 'opUserId', 'refundReason', 'type', 'refundAccount'), $result);
return $result;
}
/**
* 小程序商户退款
* @param $orderNo //微信支付单号
* @param $refundNo //微信退款单号
* @param $totalFee
* @param null $refundFee
* @param array $opt
* @return \EasyWeChat\Kernel\HttpClient\Response|ResponseInterface
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws TransportExceptionInterface
*/
public function miniRefund($orderNo, $refundNo, $totalFee, $refundFee = null, array $opt = [])
{
$totalFee = floatval($totalFee);
$refundFee = floatval($refundFee);
$order = [
'openid' => $opt['open_id'],
'trade_no' => $opt['routine_order_id'],
'transaction_id' => $orderNo,
'refund_no' => $refundNo,
'total_amount' => $totalFee,
'refund_amount' => $refundFee,
];
$result = $this->order()->refundorder($order);
self::logger('使用商户订单号退款', compact('orderNo', 'refundNo', 'totalFee', 'refundFee', 'opt'), $result);
return $result;
}
/**
* 退款
* @param $orderNo
* @param array $opt
* @return bool
* @throws \Psr\SimpleCache\InvalidArgumentException|TransportExceptionInterface
*/
public function payOrderRefund($orderNo, array $opt)
{
if (isset($opt['pay_routine_open']) && $opt['pay_routine_open']) {
return $this->payMiniOrderRefund($orderNo, $opt);
}
if (!isset($opt['pay_price'])) {
throw new PayException('缺少pay_price');
}
$certPath = $this->config->certPath;
if (!$certPath) {
throw new PayException('请上传支付证书cert');
}
$keyPath = $this->config->keyPath;
if (!$keyPath) {
throw new PayException('请上传支付证书key');
}
if (!is_file($certPath)) {
throw new PayException('支付证书cert不存在');
}
if (!is_file($keyPath)) {
throw new PayException('支付证书key不存在');
}
if ($this->isV3PAy) {
return $this->payClient->refund($orderNo, $opt);
}
$totalFee = floatval(bcmul($opt['pay_price'], 100, 0));
$refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'], 100, 0)) : null;
$refundReason = $opt['desc'] ?? '';
$refundNo = $opt['refund_id'] ?? $orderNo;
$opUserId = $opt['op_user_id'] ?? null;
$type = $opt['type'] ?? 'out_trade_no';
/*仅针对老资金流商户使用
REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款)
REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款*/
$refundAccount = $opt['refund_account'] ?? 'REFUND_SOURCE_UNSETTLED_FUNDS';
try {
$res = $this->refund($orderNo, $refundNo, $totalFee, $refundFee, $opUserId, $refundReason, $type, $refundAccount);
$res = $res->toArray();
if ($res['return_code'] == 'FAIL') {
throw new PayException('退款失败:' . $res['return_msg']);
}
if (isset($res['err_code'])) {
throw new PayException('退款失败:' . $res['err_code_des']);
}
} catch (\Exception $e) {
self::error($e);
throw new PayException($e->getMessage());
}
return true;
}
/**
* 小程序商户退款
* @param $orderNo
* @param array $opt
* @return bool
* @throws TransportExceptionInterface
*/
public function payMiniOrderRefund($orderNo, array $opt)
{
if (!isset($opt['pay_price'])) {
throw new PayException('缺少pay_price');
}
if (!isset($opt['routine_order_id'])) {
throw new PayException('缺少订单单号');
}
$totalFee = floatval(bcmul($opt['pay_price'], 100, 0));
$refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'], 100, 0)) : null;
$refundNo = $opt['refund_no'];
try {
$result = $this->miniRefund($orderNo, $refundNo, $totalFee, $refundFee, $opt);
$result = $result->toArray();
if ($result['errcode'] == '0') {
return true;
} else {
throw new PayException('退款失败:' . $result['errmsg']);
}
} catch (\Exception $e) {
self::error($e);
throw new PayException($e->getMessage());
}
}
/**
* 商家转账通知
* @return Response
* User: liusl
* DateTime: 2025/2/14 下午2:53
*/
public function handleMchNotify()
{
$response = $this->payClient->handleNotify(function ($notify, $success) {
self::logger('商家转账成功回调接口', [], $notify);
if (isset($notify['transfer_bill_no']) && $success) {
$res = Event::until('pay.mchNotify', [$notify]);
if ($res) {
return $res;
} else {
return false;
}
}
});
return response($response->getContent());
}
/**
* 微信支付成功回调接口
* @return Response
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws ReflectionException
* @throws Throwable
*/
public function handleNotify()
{
if ($this->isV3PAy) {
$response = $this->payClient->handleNotify(function ($notify, $success) {
self::logger('微信支付成功回调接口', [], $notify);
if (isset($notify['out_trade_no']) && $success) {
$res = Event::until('pay.notify', [$notify]);
if ($res) {
return $res;
} else {
return false;
}
}
});
return response($response->getContent());
} else {
$message = $this->application()->getServer()->getRequestMessage();
$message = $message->toArray();
if ($this->checkSignV2($message)) {
self::logger('微信支付成功回调接口', [], $message);
Event::until('pay.notify', [$message]);
return response(XML::build(['return_code' => 'success', 'return_message' => 'OK']), 200, [], 'xml');
} else {
return response(XML::build(['return_code' => 'fail', 'return_message' => 'FAIL']), 500, [], 'xml');
}
}
}
/**
* 验签名
* @param $message
* @return bool
*/
public function checkSignV2($message)
{
$sign = $message['sign'];
unset($message['sign']);
if ($this->generate_sign($message, $this->config->key) !== $sign) {
return false;
}
return true;
}
/**
* @param array $attributes
* @param $key
* @param $encryptMethod
* @return string
*/
function generate_sign(array $attributes, $key, $encryptMethod = 'md5')
{
ksort($attributes);
$attributes['key'] = $key;
return strtoupper(call_user_func_array($encryptMethod, [urldecode(http_build_query($attributes))]));
}
/**
* 退款结果通知
* @return Response
* @throws InvalidArgumentException
* @throws ReflectionException
* @throws RuntimeException
* @throws Throwable
*/
public function handleRefundedNotify()
{
$response = $this->application()->getServer()->handleRefunded(function ($message, \Closure $next) {
self::logger('退款结果通知', [], compact('message'));
Event::until('pay.refunded.notify', [$message]);
return $next($message);
});
return response($response->serve());
}
/**
* 是否时微信付款二维码值
* @param string $authCode
* @return bool
*/
public static function isWechatAuthCode(string $authCode)
{
return preg_match('/^[0-9]{18}$/', $authCode) && in_array(substr($authCode, 0, 2), ['10', '11', '12', '13', '14', '15']);
}
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Swoole\Coroutine\Http\Client;
/**
* 携程请求返回资源
* Class SwooleResponse
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
* @package crmeb\services\wechat
*/
class SwooleResponse implements ResponseInterface
{
/**
* @var Client
*/
private $swooleResponse;
/**
* SwooleResponse constructor.
* @param Client $swooleResponse
*/
public function __construct(Client $swooleResponse)
{
$this->swooleResponse = $swooleResponse;
}
/**
* @param bool $throw
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
public function getContent(bool $throw = true): string
{
if ($throw) {
$this->checkStatusCode();
}
return $this->swooleResponse->getBody();
}
/**
* @return int
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
public function getStatusCode(): int
{
return $this->swooleResponse->getStatusCode();
}
/**
* @param bool $throw
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
public function getHeaders(bool $throw = true): array
{
if ($throw) {
$this->checkStatusCode();
}
$headers = [];
foreach ($this->swooleResponse->getHeaders() as $name => $value) {
$headers[strtolower($name)][] = $value;
}
return $headers;
}
/**
* @param bool $throw
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
public function toArray(bool $throw = true): array
{
$content = $this->getContent($throw);
if ($content === '' && $throw) {
throw new \InvalidArgumentException('The content is not valid JSON.');
}
return json_decode($content, true);
}
/**
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
public function cancel(): void
{
$this->swooleResponse->close();
}
/**
* @param string|null $type
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/24
*/
public function getInfo(string $type = null): mixed
{
if ($type === null || $type === 'http_code') {
return $this->getStatusCode();
}
$httpResponse = [
'http_code' => $this->getStatusCode(),
'response_headers' => $this->swooleResponse->getHeaders(),
'url' => $this->swooleResponse->host,
];
return $type ? $httpResponse[$type] ?? null : $httpResponse;
}
/**
* @author 等风来
* @email 136327134@qq.com
* @date 2024/1/25
*/
private function checkStatusCode()
{
$code = $this->getInfo('http_code');
if (500 <= $code) {
throw new ServerException($this);
}
if (400 <= $code) {
throw new ClientException($this);
}
if (300 <= $code) {
throw new RedirectionException($this);
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use Throwable;
/**
* 微信错误统一处理
* Class WechatException
* @package crmeb\services\wechat
*/
class WechatException extends \RuntimeException
{
/**
* WechatException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,129 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat;
use think\Collection;
use Throwable;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
/**
* 微信错误
* Class WechatResponse
* @package crmeb\services\wechat
*/
class WechatResponse extends Collection
{
/**
* @var Throwable
*/
protected Throwable $e;
/**
* 是否抛出默认错误
* @var bool
*/
protected bool $error = true;
/**
* 数据转换
* @var array
*/
protected $items = [];
/**
* WechatResponse constructor.
* @param $items
*/
public function __construct($items = [])
{
$this->items = is_object($items) && method_exists($items, 'toArray') ? $items->toArray() : $items;
$this->wechatError();
}
/**
* 错误统一处理
*/
public function wechatError()
{
if (!$this->error) {
return;
}
if (isset($this->items['errcode']) && 0 !== $this->items['errcode']) {
throw new WechatException(
ErrorMessage::getWorkMessage(
$this->items['errcode'] ?? 0,
$this->items['errmsg'] ?? null
)
);
}
}
/**
* @param bool $boole
* @return $this
*/
public function serError(bool $boole)
{
$this->error = $boole;
return $this;
}
/**
* 正确处理
* @param callable $then
* @param bool $error
* @return $this
*/
public function then(callable $then, bool $error = null)
{
$error = $error || $this->error;
if (0 !== $this->items['errcode'] && $error) {
throw new WechatException($this->items['errmsg']);
}
try {
$this->response = $then($this->items);
} catch (Throwable $e) {
$this->e = $e;
}
return $this;
}
/**
* 异常处理
* @param callable $catch
* @return $this
*/
public function catch(callable $catch)
{
if (!$this->e) {
$this->e = new WechatException('success');
}
$catch($this->e, $this->items);
return $this;
}
/**
* 获取返回值
* @return string
*/
public function getResponse()
{
return $this->response;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client;
use EasyWeChat\Kernel\Form\File;
use EasyWeChat\Kernel\Form\Form;
use EasyWeChat\Kernel\HttpClient\Response;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* 基础接口请求
* Class BaseClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\client
*/
abstract class BaseClient
{
/**
* UserClient constructor.
* @param AccessTokenAwareClient $api
*/
public function __construct(protected AccessTokenAwareClient $api)
{
}
/**
* 上传文件
* @param string $url
* @param array $files
* @param array $form
* @param array $query
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function httpUpload(string $url, array $files = [], array $form = [], array $query = [])
{
$form['media'] = File::fromPath($files['media']);
$options = Form::create($form)->toArray();
return $this->api->request(
'POST',
$url.'?'.http_build_query($query),
$options
);
}
/**
* @param string $mediaId
* @param string $uri
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function getResources(string $mediaId, string $uri)
{
return $this->api->request('GET', $uri, [
'query' => [
'media_id' => $mediaId,
],
]);
}
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\miniprogram;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\config\MiniProgramConfig;
use crmeb\services\wechat\util\AES;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use think\annotation\Inject;
/**
* 授权
* Class AuthClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\miniprogram
*/
class AuthClient extends BaseClient
{
/**
* @var MiniProgramConfig
*/
protected MiniProgramConfig $config;
/**
* AuthClient constructor.
* @param AccessTokenAwareClient $api
*/
public function __construct(AccessTokenAwareClient $api)
{
parent::__construct($api);
$this->config = app()->make(MiniProgramConfig::class);
}
/**
* 使用code换取用户信息
* @param string $code
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function session(string $code): ResponseInterface|Response
{
$params = [
'appid' => $this->config->appId,
'secret' => $this->config->secret,
'js_code' => $code,
'grant_type' => 'authorization_code',
];
return $this->api->get('sns/jscode2session', $params);
}
/**
* 解密数据
* @param string $sessionKey
* @param string $iv
* @param string $encrypted
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function decryptData(string $sessionKey, string $iv, string $encrypted): array
{
$decrypted = AES::decrypt(
base64_decode($encrypted, false),
base64_decode($sessionKey, false),
base64_decode($iv, false)
);
$decrypted = json_decode($decrypted, true);
if (!$decrypted) {
throw new WechatException('The given payload is invalid.');
}
return $decrypted;
}
/**
* 获取小程序码:适用于需要的码数量极多,或仅临时使用的业务场景
* @param string $scene
* @param array $optional
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getUnlimit(string $scene, array $optional = []): Response
{
$params = array_merge([
'scene' => $scene,
], $optional);
return $this->getStream('wxa/getwxacodeunlimit', $params);
}
/**
* 获取流文件
* @param string $endpoint
* @param array $params
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function getStream(string $endpoint, array $params): Response
{
return $this->api->request('POST', $endpoint, ['json' => $params]);
}
/**
* 获取小程序短链
* @param string $path 通过 URL Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,不可携带 query 。path 为空时会跳转小程序主页
* @param string $query 通过 URL Link 进入小程序时的query,最大1024个字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~%
* @param int $expire_type 默认值0.小程序 URL Link 失效类型,失效时间:0,失效间隔天数:1
* @param int $expire_time 到期失效的 URL Link 的失效时间,为 Unix 时间戳。生成的到期失效 URL Link 在该时间前有效。最长有效期为30天。expire_type 为 0 必填
* @param int $expire_interval 到期失效的URL Link的失效间隔天数。生成的到期失效URL Link在该间隔时间到达前有效。最长间隔天数为30天。expire_type 为 1 必填
* @throws TransportExceptionInterface User: liusl
* DateTime: 2024/11/23 下午2:46
*/
public function generateUrlLink(string $path, string $query, int $expire_type = 0, int $expire_time = 30 * 24 * 3600, int $expire_interval = 29)
{
$params = [
'path' => $path,
'query' => $query,
'expire_type' => $expire_type,
];
if ($expire_type == 0) {
$params['expire_time'] = time() + $expire_time;
} else {
$params['expire_interval'] = $expire_interval;
}
return $this->api->postJson('wxa/generate_urllink', $params);
}
}

View File

@@ -0,0 +1,304 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\miniprogram;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 直播
* Class LiveClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\miniprogram
*/
class LiveClient extends BaseClient
{
/**
* 添加直播间参数
* @var array
*/
protected array $createData = [
'name' => '', // 房间名字
'coverImg' => '', // 通过 uploadfile 上传,填写 mediaID
'startTime' => 0, // 开始时间
'endTime' => 0, // 结束时间
'anchorName' => '', // 主播昵称
'anchorWechat' => '', // 主播微信号
'shareImg' => '', //通过 uploadfile 上传,填写 mediaID
'feedsImg' => '', //通过 uploadfile 上传,填写 mediaID
'isFeedsPublic' => 1, // 是否开启官方收录,1 开启,0 关闭
'type' => 1, // 直播类型,1 推流 0 手机直播
'screenType' => 0, // 1:横屏 0:竖屏
'closeLike' => 0, // 是否 关闭点赞 1 关闭
'closeGoods' => 0, // 是否 关闭商品货架,1:关闭
'closeComment' => 0, // 是否开启评论,1:关闭
'closeReplay' => 1, // 是否关闭回放 1 关闭
'closeShare' => 0, // 是否关闭分享 1 关闭
'closeKf' => 0 // 是否关闭客服,1 关闭
];
/**
* 获取直播房间
* @param int $start
* @param int $limit
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getRooms(int $start = 0, int $limit = 10): ResponseInterface|Response
{
$params = [
'start' => $start,
'limit' => $limit,
];
return $this->api->postJson('wxa/business/getliveinfo', $params);
}
/**
* 获取直播回放
* @param int $roomId
* @param int $start
* @param int $limit
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getPlaybacks(int $roomId, int $start = 0, int $limit = 10): ResponseInterface|Response
{
$params = [
'action' => 'get_replay',
'room_id' => $roomId,
'start' => $start,
'limit' => $limit,
];
return $this->api->postJson('wxa/business/getliveinfo', $params);
}
/**
* 创建直播间
* @param array $data
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function createRoom(array $data): ResponseInterface|Response
{
$params = array_merge($this->createData, $data);
return $this->api->postJson('wxaapi/broadcast/room/create', $params);
}
/**
* 直播间导入商品
* @param int $room_id
* @param $ids
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function roomAddGoods(int $room_id, $ids): ResponseInterface|Response
{
$params = [
'ids' => $ids,
'roomId' => $room_id
];
return $this->api->postJson('wxaapi/broadcast/room/addgoods', $params);
}
/**
* 获取商品列表信息
* @param int $status
* @param int $page
* @param int $limit
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getGoodsList(int $status, int $page = 0, int $limit = 30): ResponseInterface|Response
{
$params = [
'offset' => $page * $limit,
'limit' => $limit,
'status' => $status
];
return $this->api->get('wxaapi/broadcast/goods/getapproved', $params);
}
/**
* 获取商品详情
* @param array $ids
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getGooodsInfo(array $ids): ResponseInterface|Response
{
$params = [
'goods_ids' => $ids
];
return $this->api->get('wxa/business/getgoodswarehouse', $params);
}
/**
* 商品添加并审核
* @param string $coverImgUrl
* @param string $name
* @param int $priceType
* @param string $url
* @param string $price
* @param string $price2
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function addGoods(string $coverImgUrl, string $name, int $priceType, string $url, string $price, string $price2 = ''): ResponseInterface|Response
{
$params = [
'goodsInfo' => [
'coverImgUrl' => $coverImgUrl,
'name' => $name,
'priceType' => $priceType,
'price' => $price,
'url' => $url
]
];
if ($priceType != 1) $params['goodsInfo']['price2'] = $price2;
return $this->api->postJson('wxaapi/broadcast/goods/add', $params);
}
/**
* 撤回审核
* @param int $goodsId
* @param int $auditId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function resetauditGoods(int $goodsId, int $auditId): ResponseInterface|Response
{
$params = [
'goodsId' => $goodsId,
'auditId' => $auditId
];
return $this->api->postJson('wxaapi/broadcast/goods/resetaudit', $params);
}
/**
* 重新提交审核
* @param int $goodsId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function auditGoods(int $goodsId): ResponseInterface|Response
{
$params = [
'goodsId' => $goodsId
];
return $this->api->postJson('wxaapi/broadcast/goods/autdit', $params);
}
/**
* 删除商品
* @param int $goodsId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function deleteGoods(int $goodsId): ResponseInterface|Response
{
$params = [
'goodsId' => $goodsId
];
return $this->api->postJson('wxaapi/broadcast/goods/delete', $params);
}
/**
* 更新商品
* @param int $goodsId
* @param string $coverImgUrl
* @param string $name
* @param int $priceType
* @param string $url
* @param string $price
* @param string $price2
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function updateGoods(int $goodsId, string $coverImgUrl, string $name, int $priceType, string $url, string $price, string $price2 = ''): ResponseInterface|Response
{
$params = ['goodsInfo' => [
'goodsId' => $goodsId,
'coverImgUrl' => $coverImgUrl,
'name' => $name,
'priceType' => $priceType,
'price' => $price,
'url' => $url
]];
if ($priceType != 1) $params['goodsInfo']['price2'] = $price2;
return $this->api->postJson('wxaapi/broadcast/goods/update', $params);
}
/**
* 获取成员列表
* @param int $role
* @param int $page
* @param int $limit
* @param string $keyword
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getRoleList(int $role = 2, int $page = 0, int $limit = 30, string $keyword = ''): ResponseInterface|Response
{
$params = [
'role' => $role,
'offset' => $page * $limit,
'limit' => $limit,
'keyword' => $keyword
];
return $this->api->get('wxaapi/broadcast/role/getrolelist', $params);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\miniprogram;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* 附件
* Class MaterialClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
* @package crmeb\services\wechat\client\miniprogram
*/
class MaterialClient extends BaseClient
{
/**
* 附件类型
* @var array|string[]
*/
protected array $allowTypes = ['image', 'voice', 'video', 'thumb'];
/**
* 上传
* @param string $type
* @param string $path
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function upload(string $type, string $path): Response
{
if (!file_exists($path) || !is_readable($path)) {
throw new WechatException(sprintf("File does not exist, or the file is unreadable: '%s'", $path));
}
if (!in_array($type, $this->allowTypes, true)) {
throw new WechatException(sprintf("Unsupported media type: '%s'", $type));
}
return $this->httpUpload('cgi-bin/media/upload', ['media' => $path], ['type' => $type]);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\miniprogram;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\config\MiniProgramConfig;
use crmeb\services\wechat\util\AES;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use think\annotation\Inject;
use think\exception\ValidateException;
class MediaCheckClient extends BaseClient
{
const MSG_API = 'wxa/msg_sec_check';
const MEDIA_API = 'wxa/media_check_async';
const LABEL = [
100 => '正常',
10001 => '广告',
20001 => '时政',
20002 => '色情',
20003 => '辱骂',
20006 => '违法犯罪',
20008 => '欺诈',
20012 => '低俗',
20013 => '版权',
21000 => '其他'
];
//scene 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志)
/**
* 文本敏感词检测
* @param $content
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* User: liusl
* DateTime: 2024/11/26 下午5:28
*/
public function msgSecCheck($content)
{
$params = [
'content' => $content
];
return $this->api->postJson(self::MSG_API, $params);
}
}

View File

@@ -0,0 +1,424 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\miniprogram;
use Redis;
use crmeb\exceptions\AdminException;
use crmeb\services\wechat\config\MiniProgramConfig;
use crmeb\services\wechat\config\PaymentConfig;
use crmeb\services\wechat\util\Helper;
use crmeb\services\wechat\WechatException;
use crmeb\services\wechat\WechatResponse;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface as ResponseInterfaceAlias;
use think\facade\Cache;
/**
* 订单管理
* Class OrderClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\miniprogram
*/
class OrderClient
{
const redis_prefix = 'mini_order';
const express_company = 'ZTO'; // 默认发货快递公司为(中通快递)
/**
* @var PaymentConfig
*/
protected PaymentConfig $config;
/**
* @var MiniProgramConfig
*/
protected MiniProgramConfig $miniProgramConfig;
/**
* @var Redis
*/
protected Redis $redis;
/**
* UserClient constructor.
* @param AccessTokenAwareClient $api
*/
public function __construct(protected AccessTokenAwareClient $api)
{
$this->config = app()->make(PaymentConfig::class);
$this->miniProgramConfig = app()->make(MiniProgramConfig::class);
}
/**
* 处理联系人
* @param array $contact
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function handleContact(array $contact = []): array
{
if (isset($contact)) {
if (isset($contact['consignor_contact']) && $contact['consignor_contact']) {
$contact['consignor_contact'] = Helper::encryptTel($contact['consignor_contact']);
}
if (isset($contact['receiver_contact']) && $contact['receiver_contact']) {
$contact['receiver_contact'] = Helper::encryptTel($contact['receiver_contact']);
}
}
return $contact;
}
/**
* 上传订单
* @param string $out_trade_no
* @param int $logistics_type
* @param array $shipping_list
* @param string $payer_openid
* @param $path
* @param int $delivery_mode
* @param bool $is_all_delivered
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function shippingByTradeNo(string $out_trade_no, int $logistics_type, array $shipping_list, string $payer_openid, $path, int $delivery_mode = 1, bool $is_all_delivered = true)
{
if (!$this->checkManaged()) {
throw new AdminException('开通小程序订单管理服务后重试');
}
$params = [
'order_key' => [
'order_number_type' => 2,
'mchid' => $this->config->mchId,
'transaction_id' => $out_trade_no
],
'logistics_type' => $logistics_type,
'delivery_mode' => $delivery_mode,
'upload_time' => date(DATE_RFC3339),
'payer' => [
'openid' => $payer_openid
]
];
if ($delivery_mode == 2) {
$params['is_all_delivered'] = $is_all_delivered;
}
foreach ($shipping_list as $shipping) {
$contact = $this->handleContact($shipping['contact'] ?? []);
$params['shipping_list'][] = [
'tracking_no' => $shipping['tracking_no'] ?? '',
'express_company' => $shipping['express_company'] ?? '',
'item_desc' => $shipping['item_desc'],
'contact' => $contact
];
}
return $this->shipping($params);
}
/**
* 合单
* @param string $out_trade_no
* @param int $logistics_type
* @param array $sub_orders
* @param string $payer_openid
* @param int $delivery_mode
* @param bool $is_all_delivered
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function combinedShippingByTradeNo(string $out_trade_no, int $logistics_type, array $sub_orders, string $payer_openid, int $delivery_mode = 2, bool $is_all_delivered = false)
{
if (!$this->checkManaged()) {
throw new WechatException('开通小程序订单管理服务后重试');
}
$params = [
'order_key' => [
'order_number_type' => 1,
'mchid' => $this->config->mchId,
'out_trade_no' => $out_trade_no,
],
'upload_time' => date(DATE_RFC3339),
'payer' => [
'openid' => $payer_openid
]
];
foreach ($sub_orders as $order) {
$sub_order = [
'order_key' => [
'order_number_type' => 1,
'mchid' => $this->config->mchId,
'out_trade_no' => $order['out_trade_no'],
'logistics_type' => $logistics_type,
],
'delivery_mode' => $delivery_mode,
'is_all_delivered' => $is_all_delivered
];
foreach ($sub_orders['shipping_list'] as $shipping) {
$contact = $this->handleContact($shipping['contact'] ?? []);
$sub_order['shipping_list'][] = [
'tracking_no' => $shipping['tracking_no'] ?? '',
'express_company' => isset($shipping['express_company']) ? $this->getDelivery($shipping['express_company']) : '',
'item_desc' => $shipping['item_desc'],
'contact' => $contact
];
}
$params['sub_orders'][] = $sub_order;
}
return $this->resultHandle($this->api->postJson('wxa/sec/order/upload_combined_shipping_info', $params));
}
/**
* 签收通知
* @param string $merchant_trade_no
* @param string $received_time
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function notifyConfirmByTradeNo(string $merchant_trade_no, string $received_time)
{
$params = [
'merchant_id' => $this->config->mchId,
'merchant_trade_no' => $merchant_trade_no,
'received_time' => $received_time
];
return $this->resultHandle($this->api->postJson('wxa/sec/order/notify_confirm_receive', $params));
}
/**
* 设置小修跳转路径
* @param $path
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function setMesJumpPathAndCheck($path)
{
if (!$this->checkManaged()) {
throw new WechatException('开通小程序订单管理服务后重试');
}
$params = [
'path' => $path
];
return $this->resultHandle($this->api->postJson('wxa/sec/order/set_msg_jump_path', $params));
}
/**
* @param $params
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function shipping($params)
{
return $this->resultHandle($this->api->postJson('wxa/sec/order/upload_shipping_info', $params));
}
/**
* @return object|Redis|null
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function getRedis()
{
if (empty($this->redis)) {
$this->redis = Cache::store('redis')->handler();
}
return $this->redis;
}
/**
* 查询小程序是否已开通发货信息管理服务
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function isManaged()
{
$params = [
'appid' => $this->miniProgramConfig->appId
];
return $this->resultHandle($this->api->postJson('wxa/sec/order/is_trade_managed', $params));
}
/**
* @param Response|ResponseInterfaceAlias $result
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
private function resultHandle(Response|ResponseInterfaceAlias $result)
{
return new WechatResponse($result);
}
/**
* 获取运力id列表get_delivery_list
* @return mixed
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getDeliveryList()
{
return $this->resultHandle($this->api->postJson('cgi-bin/express/delivery/open_msg/get_delivery_list', []));
}
/**
* 设置物流列表
* @return array
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function setDeliveryList()
{
$list = $this->getDeliveryList();
if ($list) {
$key = self::redis_prefix . '_delivery_list';
$date = array_column($list['delivery_list'], 'delivery_id', 'delivery_name');
// 创建缓存
$this->getRedis()->hMSet($key, $date);
return $date;
} else {
throw new WechatException('物流公司列表异常');
}
}
/**
* @return bool
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function checkManaged()
{
$key = self::redis_prefix . '_is_trade_managed';
if ($this->getRedis()->exists($key)) {
return true;
} else {
return $this->setManaged();
}
}
/**
* @return bool
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function setManaged()
{
try {
$res = $this->isManaged();
if ($res['is_trade_managed']) {
$key = self::redis_prefix . '_is_trade_managed';
$this->getRedis()->set($key, $res['is_trade_managed']);
return true;
} else {
return false;
}
} catch (\Throwable) {
return false;
}
}
/**
* 获取物流列表
* @param $company_name
* @return mixed|string
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getDelivery($company_name)
{
$key = self::redis_prefix . '_delivery_list';
if (!$this->getRedis()->exists($key)) {
$date = $this->setDeliveryList();
$express_company = $date[$company_name] ?? '';
} else {
$express_company = $this->getRedis()->hMGet($key, [$company_name])[$company_name] ?? '';
}
if (empty($express_company)) {
$express_company = self::express_company;
}
return $express_company;
}
}

View File

@@ -0,0 +1,158 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\miniprogram;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class SubscribeClient extends BaseClient
{
/**
* @var array
*/
protected array $message = [
'touser' => '',
'template_id' => '',
'page' => '',
'data' => [],
'miniprogram_state' => 'formal',
];
/**
* @var string[]
*/
protected array $required = ['touser', 'template_id', 'data'];
/**
* 添加订阅消息模版
* @param string $tid
* @param array $kidList
* @param string|null $sceneDesc
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function addTemplate(string $tid, array $kidList, string $sceneDesc = null): ResponseInterface|Response
{
$sceneDesc = $sceneDesc ?? '';
$data = compact('tid', 'kidList', 'sceneDesc');
return $this->api->post('wxaapi/newtmpl/addtemplate', $data);
}
/**
* 删除模板消息
* @param string $id
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function deleteTemplate(string $id): ResponseInterface|Response
{
return $this->api->post('wxaapi/newtmpl/deltemplate', ['priTmplId' => $id]);
}
/**
* 获取模版标题的关键词列表
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @param string $tid
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
*/
public function getTemplateKeywords(string $tid): ResponseInterface|Response
{
return $this->api->get('wxaapi/newtmpl/getpubtemplatekeywords', compact('tid'));
}
/**
* 发送消息
* @param array $data
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function send(array $data = []): ResponseInterface|Response
{
$params = $this->formatMessage($data);
$this->restoreMessage();
return $this->api->postJson('cgi-bin/message/subscribe/send', $params);
}
/**
* @param array $data
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function formatMessage(array $data = []): array
{
$params = array_merge($this->message, $data);
foreach ($params as $key => $value) {
if (in_array($key, $this->required, true) && empty($value) && empty($this->message[$key])) {
throw new WechatException(sprintf('Attribute "%s" can not be empty!', $key));
}
$params[$key] = empty($value) ? $this->message[$key] : $value;
}
foreach ($params['data'] as $key => $value) {
if (is_array($value)) {
if (\array_key_exists('value', $value)) {
$params['data'][$key] = ['value' => $value['value']];
continue;
}
if (count($value) >= 1) {
$value = [
'value' => $value[0],
];
}
} else {
$value = [
'value' => strval($value),
];
}
$params['data'][$key] = $value;
}
return $params;
}
/**
* Restore message.
*/
protected function restoreMessage()
{
$this->message = (new \ReflectionClass(static::class))->getDefaultProperties()['message'];
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\official;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 卡片
* Class CardClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\client\official
*/
class CardClient extends BaseClient
{
const STATUS_CONTENT = 'CARD_STATUS_VERIFY_OK';
/**
* 获取会员卡列表
* @param int $offset
* @param int $count
* @param string $statusList
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function list(int $offset = 0, int $count = 10, string $statusList = self::STATUS_CONTENT): ResponseInterface|Response
{
$params = [
'offset' => $offset,
'count' => $count,
'status_list' => $statusList,
];
return $this->api->postJson('card/batchget', $params);
}
/**
* 获取颜色
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function colors(): ResponseInterface|Response
{
return $this->api->get('card/getcolors');
}
/**
* 创建卡卷
* @param string $cardType
* @param array $attributes
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function create(string $cardType = 'member_card', array $attributes = []): ResponseInterface|Response
{
$params = [
'card' => [
'card_type' => strtoupper($cardType),
strtolower($cardType) => $attributes,
],
];
return $this->api->postJson('card/create', $params);
}
/**
* 获取卡卷信息
* @param string $cardId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function get(string $cardId): ResponseInterface|Response
{
$params = [
'card_id' => $cardId,
];
return $this->api->postJson('card/get', $params);
}
/**
* 修改卡卷
* @param string $cardId
* @param string $type
* @param array $attributes
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function update(string $cardId, string $type, array $attributes = []): ResponseInterface|Response
{
$card = [];
$card['card_id'] = $cardId;
$card[strtolower($type)] = $attributes;
return $this->api->postJson('card/update', $card);
}
/**
* 删除卡卷
* @param string $cardId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function delete(string $cardId): ResponseInterface|Response
{
$params = [
'card_id' => $cardId,
];
return $this->api->postJson('card/delete', $params);
}
/**
* 创建二维码.
* @param array $cards
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function createQrCode(array $cards): ResponseInterface|Response
{
return $this->api->postJson('card/qrcode/create', $cards);
}
/**
* 设置开卡字段接口.
* @param string $cardId
* @param array $settings
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function setActivationForm(string $cardId, array $settings): ResponseInterface|Response
{
$params = array_merge(['card_id' => $cardId], $settings);
return $this->api->postJson('card/membercard/activateuserform/set', $params);
}
/**
* 会员卡接口激活.
* @param array $info
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function activate(array $info = []): ResponseInterface|Response
{
return $this->api->postJson('card/membercard/activate', $info);
}
/**
* 拉取会员信息接口.
* @param string $cardId
* @param string $code
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function getUser(string $cardId, string $code): ResponseInterface|Response
{
$params = [
'card_id' => $cardId,
'code' => $code,
];
return $this->api->postJson('card/membercard/userinfo/get', $params);
}
/**
* 更新会员信息.
* @param array $params
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function updateUser(array $params = []): ResponseInterface|Response
{
return $this->api->postJson('card/membercard/updateuser', $params);
}
}

View File

@@ -0,0 +1,98 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\official;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* 永久素材上传-附件
* Class MaterialClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\client\official
*/
class MaterialClient extends BaseClient
{
/**
* 上传图片
* @param string $path
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function uploadImage(string $path): Response
{
return $this->upload('image', $path);
}
/**
* 上传音频
* @param string $path
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function uploadVoice(string $path): Response
{
return $this->upload('voice', $path);
}
/**
* 上传文件
* @param string $type
* @param string $path
* @param array $form
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function upload(string $type, string $path, array $form = []): Response
{
if (!file_exists($path) || !is_readable($path)) {
throw new WechatException(sprintf('File does not exist, or the file is unreadable: "%s"', $path));
}
$form['type'] = $type;
return $this->httpUpload($this->getApiByType($type), ['media' => $path], $form);
}
/**
* 获取接口
* @param string $type
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function getApiByType(string $type): string
{
return match ($type) {
'news_image' => 'cgi-bin/media/uploadimg',
default => 'cgi-bin/material/add_material'
};
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\official;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\WechatException;
class MediaClient extends BaseClient
{
/**
* Allow media type.
*
* @var array
*/
protected $allowTypes = ['image', 'voice', 'video', 'thumb'];
public function uploadImage($path)
{
return $this->upload('image', $path);
}
public function uploadVideo($path)
{
return $this->upload('video', $path);
}
public function uploadVoice($path)
{
return $this->upload('voice', $path);
}
public function uploadThumb($path)
{
return $this->upload('thumb', $path);
}
public function upload(string $type, string $path)
{
if (!file_exists($path) || !is_readable($path)) {
throw new WechatException(sprintf("File does not exist, or the file is unreadable: '%s'", $path));
}
if (!in_array($type, $this->allowTypes, true)) {
throw new WechatException(sprintf("Unsupported media type: '%s'", $type));
}
return $this->httpUpload('cgi-bin/media/upload', ['media' => $path], ['type' => $type]);
}
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\official;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 二维码
* Class QrCodeClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\client\official
*/
class QrCodeClient extends BaseClient
{
public const DAY = 86400;
public const SCENE_MAX_VALUE = 100000;
public const SCENE_QR_CARD = 'QR_CARD';
public const SCENE_QR_TEMPORARY = 'QR_SCENE';
public const SCENE_QR_TEMPORARY_STR = 'QR_STR_SCENE';
public const SCENE_QR_FOREVER = 'QR_LIMIT_SCENE';
public const SCENE_QR_FOREVER_STR = 'QR_LIMIT_STR_SCENE';
/**
* 获取url
* @param string $ticket
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function url(string $ticket): string
{
return sprintf('https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s', urlencode($ticket));
}
/**
* createForeverQrcode
* @param string|int $sceneValue
* @return array|mixed[]
* @throws TransportExceptionInterface
* @throws \EasyWeChat\Kernel\Exceptions\BadResponseException
* @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
*/
public function forever(string|int $sceneValue): array
{
if (is_int($sceneValue) && $sceneValue > 0 && $sceneValue < self::SCENE_MAX_VALUE) {
$type = self::SCENE_QR_FOREVER;
$sceneKey = 'scene_id';
} else {
$type = self::SCENE_QR_FOREVER_STR;
$sceneKey = 'scene_str';
}
$scene = [$sceneKey => $sceneValue];
return $this->create($type, $scene, false)->toArray();
}
/**
* 临时二维码生成
* @param int|string $sceneValue
* @param int|null $expireSeconds
* @return array|mixed[]
* @throws TransportExceptionInterface
* @throws \EasyWeChat\Kernel\Exceptions\BadResponseException
* @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
*/
public function temporary(int|string $sceneValue, ?int $expireSeconds = null): array
{
if (is_int($sceneValue) && $sceneValue > 0) {
$type = self::SCENE_QR_TEMPORARY;
$sceneKey = 'scene_id';
} else {
$type = self::SCENE_QR_TEMPORARY_STR;
$sceneKey = 'scene_str';
}
$scene = [$sceneKey => $sceneValue];
return $this->create($type, $scene, true, $expireSeconds)->toArray();
}
/**
* 创建二维码
* @param $actionName
* @param $actionInfo
* @param bool $temporary
* @param null $expireSeconds
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function create($actionName, $actionInfo, bool $temporary = true, $expireSeconds = null): ResponseInterface|Response
{
null !== $expireSeconds || $expireSeconds = 7 * self::DAY;
$params = [
'action_name' => $actionName,
'action_info' => ['scene' => $actionInfo],
];
if ($temporary) {
$params['expire_seconds'] = min($expireSeconds, 30 * self::DAY);
}
return $this->api->postJson('cgi-bin/qrcode/create', $params);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\official;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\message\MessageInterface;
use crmeb\services\wechat\message\Raw;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 消息
* Class StaffClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
* @package crmeb\services\wechat\client\offical
*/
class StaffClient extends BaseClient
{
/**
* 发送消息
* @param MessageInterface|string $message
* @param string $to
* @param string $account
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function send(MessageInterface|string $message, string $to, string $account = ''): ResponseInterface|Response
{
if (empty($message)) {
throw new \RuntimeException('No message to send.');
}
if ($message instanceof Raw) {
$message = json_decode($message->get('content'), true);
} else {
$prepends = [
'touser' => $to,
];
if ($account) {
$prepends['customservice'] = ['kf_account' => $account];
}
$message = $message->transformForJsonRequest($prepends);
}
return $this->api->postJson('cgi-bin/message/custom/send', $message);
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\official;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 模板消息
* Class TemplateClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\client\official
*/
class TemplateClient extends BaseClient
{
/**
* @var array
*/
protected array $message = [
'touser' => '',
'template_id' => '',
'url' => '',
'data' => [],
'miniprogram' => '',
];
/**
* @var array|string[]
*/
protected array $required = ['touser', 'template_id'];
/**
* 设置行业
* @param string $industryOne
* @param string $industryTwo
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function setIndustry(string $industryOne, string $industryTwo): ResponseInterface|Response
{
$params = [
'industry_id1' => $industryOne,
'industry_id2' => $industryTwo,
];
return $this->api->postJson('cgi-bin/template/api_set_industry', $params);
}
/**
* 添加模板消息
* @param int $shortId
* @param array $keywordList
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function addTemplate($shortId, $keywordList): ResponseInterface|Response
{
$params = ['template_id_short' => $shortId, 'keyword_name_list' => $keywordList];
return $this->api->postJson('cgi-bin/template/api_add_template', $params);
}
/**
* 获取模板消息
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function getPrivateTemplates(): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/template/get_all_private_template');
}
/**
* 删除模板
* @param string $templateId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function deletePrivateTemplate(string $templateId): ResponseInterface|Response
{
$params = ['template_id' => $templateId];
return $this->api->postJson('cgi-bin/template/del_private_template', $params);
}
/**
* 获取行业
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function getIndustry()
{
return $this->api->postJson('cgi-bin/template/get_industry');
}
/**
* 发送模板消息
* @param array $data
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function send(array $data = [])
{
$params = $this->formatMessage($data);
$this->restoreMessage();
return $this->api->postJson('cgi-bin/message/template/send', $params);
}
protected function restoreMessage()
{
$this->message = (new \ReflectionClass(static::class))->getDefaultProperties()['message'];
}
/**
* 获取
* @param array $data
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
protected function formatMessage(array $data = [])
{
$params = array_merge($this->message, $data);
foreach ($params as $key => $value) {
if (in_array($key, $this->required, true) && empty($value) && empty($this->message[$key])) {
throw new WechatException(sprintf('Attribute "%s" can not be empty!', $key));
}
$params[$key] = empty($value) ? $this->message[$key] : $value;
}
$params['data'] = $this->formatData($params['data'] ?? []);
return $params;
}
/**
* 获取data
* @param array $data
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
protected function formatData(array $data)
{
$formatted = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
if (\array_key_exists('value', $value)) {
$formatted[$key] = $value;
continue;
}
if (count($value) >= 2) {
$value = [
'value' => $value[0],
'color' => $value[1],
];
}
} else {
$value = [
'value' => strval($value),
];
}
$formatted[$key] = $value;
}
return $formatted;
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\official;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\AccessTokenAwareClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 用户接口
* Class UserClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
* @package crmeb\services\wechat\offical\user
*/
class UserClient extends BaseClient
{
/**
* 获取单个用户
* @param string $openid
* @param string $lang
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function get(string $openid, string $lang = 'zh_CN'): ResponseInterface|Response
{
$params = [
'openid' => $openid,
'lang' => $lang,
];
return $this->api->get('cgi-bin/user/info', $params);
}
/**
* 创建菜单
* @param array $buttons
* @param array $matchRule
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function menuCreate(array $buttons, array $matchRule = []): ResponseInterface|Response
{
if (!empty($matchRule)) {
return $this->api->postJson('cgi-bin/menu/addconditional', [
'button' => $buttons,
'matchrule' => $matchRule,
]);
}
return $this->api->postJson('cgi-bin/menu/create', ['button' => $buttons]);
}
/**
* 批量获取用户
* @param string|null $nextOpenId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function list(string $nextOpenId = null): ResponseInterface|Response
{
$params = ['next_openid' => $nextOpenId];
return $this->api->get('cgi-bin/user/get', $params);
}
/**
* 搜索用户
* @param array $openids
* @param string $lang
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function select(array $openids, string $lang = 'zh_CN'): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/user/info/batchget', [
'user_list' => array_map(function ($openid) use ($lang) {
return [
'openid' => $openid,
'lang' => $lang,
];
}, $openids),
]);
}
}

View File

@@ -0,0 +1,339 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\pay;
use crmeb\services\wechat\util\Helper;
use crmeb\services\wechat\util\XML;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use think\helper\Arr;
use EasyWeChat\Pay\Client;
use EasyWeChat\Kernel\Contracts\Config;
/**
* 创建订单
* Class OrderClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
* @package crmeb\services\wechat\client\pay
*/
class OrderClient
{
/**
* OrderClient constructor.
* @param Client $api
* @param Config $config
*/
public function __construct(protected Client $api, protected Config $config)
{
}
/**
* 小程序创建订单
* @param array $params
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function createorder(array $params): ResponseInterface|Response
{
$data = [
'openid' => $params['openid'],//支付者的openid
'combine_trade_no' => $params['out_trade_no'],//商家合单支付总交易单号
'expire_time' => time() + 7000,
'sub_orders' => [
[
'mchid' => $this->config->get('mch_id'),
'amount' => (int)$params['total_fee'],
'trade_no' => $params['out_trade_no'],
'description' => $params['body'],
]
],
];
return $this->api->postJson('shop/pay/createorder', $data);
}
/**
* 小程序退款
* @param array $params
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function refundorder(array $params): ResponseInterface|Response
{
$data = [
'openid' => $params['openid'],
'mchid' => $this->config->get('mch_id'),
'trade_no' => $params['trade_no'],
'transaction_id' => $params['transaction_id'],
'refund_no' => $params['refund_no'],
'total_amount' => (int)$params['total_amount'],
'refund_amount' => (int)$params['refund_amount'],
];
return $this->api->postJson('shop/pay/refundorder', $data);
}
/**
* 付款码支付
* @param array $params
* @return ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function pay(array $params): ResponseInterface
{
$params['appid'] = $this->config->get('app_id');
return $this->api->request('post', 'pay/micropay', $params);
}
/**
* 下单
* @param array $params
* @return ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function unify(array $params): ResponseInterface
{
if (empty($params['spbill_create_ip'])) {
$params['spbill_create_ip'] = request()->ip();
}
$params['notify_url'] = $params['notify_url'] ?? $this->config->get('notify_url');
return $this->request('pay/unifiedorder', $params);
}
/**
* 发起请求
* @param string $endpoint
* @param array $params
* @param string $method
* @param array $options
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
protected function request(string $endpoint, array $params = [], string $method = 'post', array $options = []): ResponseInterface
{
$base = [
'appid' => $this->config->get('app_id'),
'mch_id' => $this->config->get('mch_id'),
'nonce_str' => uniqid(),
];
$params = array_filter(array_filter(array_merge($base, [], $params)), 'strlen');
$secretKey = $this->config->get('v2_secret_key');
$encryptMethod = Helper::getEncryptMethod(Arr::get($params, 'sign_type', 'MD5'), $secretKey);
$params['sign'] = Helper::generateSign($params, $secretKey, $encryptMethod);
$options = array_merge([
'body' => XML::build($params),
], $options);
return $this->api->request(strtoupper($method), $endpoint, $options);
}
/**
* 商户订单号退款
* @param string $number
* @param string $refundNumber
* @param int $totalFee
* @param int $refundFee
* @param array $optional
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function byOutTradeNumber(string $number, string $refundNumber, int $totalFee, int $refundFee, array $optional = []): Response
{
return $this->refund($refundNumber, $totalFee, $refundFee, array_merge($optional, ['out_trade_no' => $number]));
}
/**
* 支付回执单号退款
* @param string $transactionId
* @param string $refundNumber
* @param int $totalFee
* @param int $refundFee
* @param array $optional
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function byTransactionId(string $transactionId, string $refundNumber, int $totalFee, int $refundFee, array $optional = []): Response
{
return $this->refund($refundNumber, $totalFee, $refundFee, array_merge($optional, ['transaction_id' => $transactionId]));
}
/**
* 退款
* @param string $refundNumber
* @param int $totalFee
* @param int $refundFee
* @param array $optional
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function refund(string $refundNumber, int $totalFee, int $refundFee, array $optional = []): Response
{
$params = array_merge([
'out_refund_no' => $refundNumber,
'total_fee' => $totalFee,
'refund_fee' => $refundFee,
'appid' => $this->config->get('app_id'),
], $optional);
return $this->safeRequest('secapi/pay/refund', $params);
}
/**
* 撤销订单
* @param string $outTradeNumber
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function reverseByOutTradeNumber(string $outTradeNumber): Response
{
return $this->reverse($outTradeNumber, 'out_trade_no');
}
/**
* 撤销订单
* @param string $number
* @param string $type
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function reverse(string $number, string $type): Response
{
$params = [
'appid' => $this->config->get('app_id'),
$type => $number,
];
return $this->safeRequest('secapi/pay/reverse', $params);
}
/**
* 查询退款订单
* @param string $number
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function queryByOutTradeNumber(string $number): Response
{
return $this->query([
'out_trade_no' => $number,
]);
}
/**
* 查询订单
* @param array $params
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function query(array $params): Response
{
$params['appid'] = $this->config->get('app_id');
return $this->request('pay/orderquery', $params);
}
/**
* 企业付款到零钱
* @param array $params
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function toBalance(array $params): Response
{
$base = [
'mch_id' => null,
'mchid' => $this->config->get('mch_id'),
'mch_appid' => $this->config->get('app_id'),
];
if (empty($params['spbill_create_ip'])) {
$params['spbill_create_ip'] = request()->ip();
}
return $this->safeRequest('mmpaymkttransfers/promotion/transfers', array_merge($base, $params));
}
/**
* 退款请求
* @param string $endpoint
* @param array $params
* @param string $method
* @param array $options
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function safeRequest(string $endpoint, array $params, string $method = 'post', array $options = []): Response
{
$options = array_merge([
'cert' => $this->config->get('certificate'),
'ssl_key' => $this->config->get('private_key'),
], $options);
return $this->request($endpoint, $params, $method, $options);
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 联系客服
* Class ContactWayClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/19
* @package crmeb\services\wechat\client\work
*/
class ContactWayClient extends BaseClient
{
/**
* 配置客户联系「联系我」方式.
* @param int $type
* @param int $scene
* @param array $config
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function create(int $type, int $scene, array $config = []): ResponseInterface|Response
{
$params = array_merge([
'type' => $type,
'scene' => $scene,
], $config);
return $this->api->postJson('cgi-bin/externalcontact/add_contact_way', $params);
}
/**
* 更新企业已配置的「联系我」方式.
* @param string $configId
* @param array $config
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function update(string $configId, array $config = []): ResponseInterface|Response
{
$params = array_merge([
'config_id' => $configId,
], $config);
return $this->api->postJson('cgi-bin/externalcontact/update_contact_way', $params);
}
/**
* 删除企业已配置的「联系我」方式.
* @param string $configId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function delete(string $configId): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/del_contact_way', [
'config_id' => $configId,
]);
}
/**
* 获取企业已配置的「联系我」列表,注意,该接口仅可获取2021年7月10日以后创建的「联系我」
* @param string $cursor 分页查询使用的游标,为上次请求返回的 next_cursor
* @param int $limit 每次查询的分页大小,默认为100条,最多支持1000条
* @param int|null $startTime 「联系我」创建起始时间戳, 不传默认为90天前
* @param int|null $endTime 「联系我」创建结束时间戳, 不传默认为当前时间
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function list(string $cursor = '', int $limit = 100, int $startTime = null, int $endTime = null): ResponseInterface|Response
{
$data = [
'cursor' => $cursor,
'limit' => $limit,
];
if ($startTime) {
$data['start_time'] = $startTime;
}
if ($endTime) {
$data['end_time'] = $endTime;
}
return $this->api->postJson('cgi-bin/externalcontact/list_contact_way', $data);
}
/**
* 结束临时会话
* @param string $userId 企业成员的user_id
* @param string $externalUserId 客户的外部联系人user_id
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function closeTempChat(string $userId, string $externalUserId): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/close_temp_chat', [
'userid' => $userId,
'external_userid' => $externalUserId
]);
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 部门
* Class DepartmentClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\work
*/
class DepartmentClient extends BaseClient
{
/**
* 获取部门信息
* @param int $id
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function get(int $id): ResponseInterface|Response
{
return $this->api->get('cgi-bin/department/get', compact('id'));
}
/**
* 获取部门列表
* @param int|null $id
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function list(?int $id = null): ResponseInterface|Response
{
return $this->api->get('cgi-bin/department/list', compact('id'));
}
/**
* 获取子部门ID列表
* @param int|null $id
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function simpleList(?int $id = null): ResponseInterface|Response
{
return $this->api->get('cgi-bin/department/simplelist', compact('id'));
}
/**
* 获取成员ID列表
* @param int $limit
* @param string $cursor
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getUserListIds(int $limit = 0, string $cursor = ''): ResponseInterface|Response
{
$data = [];
if ($limit) {
$data['limit'] = $limit;
}
if ($cursor) {
$data['cursor'] = $cursor;
}
return $this->api->postJson('cgi-bin/user/list_id', $data);
}
}

View File

@@ -0,0 +1,222 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 外部联系
* Class ExternalContactClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\work
*/
class ExternalContactClient extends BaseClient
{
/**
* 获取外部联系人详情
* @param string $externalUserId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function get(string $externalUserId): ResponseInterface|Response
{
return $this->api->get('cgi-bin/externalcontact/get', [
'external_userid' => $externalUserId,
]);
}
/**
* 批量获取客户详情
* @param array $userIdList
* @param string $cursor
* @param int $limit
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function batchGet(array $userIdList, string $cursor = '', int $limit = 100): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/batch/get_by_user', [
'userid_list' => $userIdList,
'cursor' => $cursor,
'limit' => $limit,
]);
}
/**
* 获取企业标签库
* @param array $tagIds
* @param array $groupIds
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getCorpTags(array $tagIds = [], array $groupIds = []): ResponseInterface|Response
{
$params = [
'tag_id' => $tagIds,
'group_id' => $groupIds
];
return $this->api->postJson('cgi-bin/externalcontact/get_corp_tag_list', $params);
}
/**
* 添加企业客户标签
* @param array $params
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function addCorpTag(array $params): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/add_corp_tag', $params);
}
/**
* 编辑企业客户标签
* @param string $id
* @param string|null $name
* @param int|null $order
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function updateCorpTag(string $id, ?string $name = null, ?int $order = null): ResponseInterface|Response
{
$params = [
"id" => $id
];
if (!\is_null($name)) {
$params['name'] = $name;
}
if (!\is_null($order)) {
$params['order'] = $order;
}
return $this->api->postJson('cgi-bin/externalcontact/edit_corp_tag', $params);
}
/**
* 删除企业客户标签
* @param array $tagId
* @param array $groupId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function deleteCorpTag(array $tagId, array $groupId): ResponseInterface|Response
{
$params = [
"tag_id" => $tagId,
"group_id" => $groupId,
];
return $this->api->postJson('cgi-bin/externalcontact/del_corp_tag', $params);
}
/**
* 编辑客户企业标签
* @param array $params
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function markTags(array $params): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/mark_tag', $params);
}
/**
* 获取客户群列表
* @param array $params
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getGroupChats(array $params): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/groupchat/list', $params);
}
/**
* 获取客户群详情
* @param string $chatId
* @param int $needName
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getGroupChat(string $chatId, int $needName = 0): ResponseInterface|Response
{
$params = [
'chat_id' => $chatId,
'need_name' => $needName,
];
return $this->api->postJson('cgi-bin/externalcontact/groupchat/get', $params);
}
/**
* 获取「群聊数据统计」数据. (按自然日聚合的方式)
* @param int $dayBeginTime
* @param int $dayEndTime
* @param array $userIds
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function groupChatStatisticGroupByDay(int $dayBeginTime, int $dayEndTime, array $userIds = []): ResponseInterface|Response
{
$params = [
'day_begin_time' => $dayBeginTime,
'day_end_time' => $dayEndTime,
'owner_filter' => [
'userid_list' => $userIds
]
];
return $this->api->postJson('cgi-bin/externalcontact/groupchat/statistic_group_by_day', $params);
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 客户群聊配置
* Class GroupChatClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\work
*/
class GroupChatClient extends BaseClient
{
/**
* 更新客户群进群方式配置
* @param string $configId
* @param string $roomBaseName
* @param array $chatIdList
* @param string $state
* @param int $autoCreateRoom
* @param int $roomBaseId
* @param string|null $remark
* @param int $scene
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function updateJoinWay(string $configId, string $roomBaseName, array $chatIdList, string $state, int $autoCreateRoom = 1, int $roomBaseId = 1, string $remark = null, int $scene = 2): ResponseInterface|Response
{
$data = [
'config_id' => $configId,
'scene' => $scene,
'remark' => $remark,
'auto_create_room' => $autoCreateRoom,
'room_base_name' => $roomBaseName,
'room_base_id' => $roomBaseId,
'chat_id_list' => $chatIdList,
'state' => $state,
];
return $this->api->postJson('cgi-bin/externalcontact/groupchat/update_join_way', $data);
}
/**
* 配置客户群进群方式
* @param string $roomName
* @param array $chatIdList
* @param string $state
* @param int $autoCreateRoom
* @param int $roomBaseId
* @param string|null $remark
* @param int $scene
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function addJoinWay(string $roomName, array $chatIdList, string $state, int $autoCreateRoom = 1, int $roomBaseId = 1, string $remark = null, int $scene = 2): ResponseInterface|Response
{
$data = [
'scene' => $scene,
'remark' => $remark,
'chat_id_list' => $chatIdList,
'auto_create_room' => $autoCreateRoom,
'room_base_name' => $roomName,
'room_base_id' => $roomBaseId,
'state' => $state
];
return $this->api->postJson('cgi-bin/externalcontact/groupchat/add_join_way', $data);
}
/**
* 获取客户群进群方式配置
* @param string $configId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getJoinWay(string $configId): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/groupchat/get_join_way', ['config_id' => $configId]);
}
/**
* 删除客户群进群方式配置
* @param string $configId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function deleteJoinWay(string $configId): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/groupchat/del_join_way', ['config_id' => $configId]);
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* 附件管理
* Class MediaClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\work
*/
class MediaClient extends BaseClient
{
/**
* @param string $type
* @param string $path
* @param array $form
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function upload(string $type, string $path, array $form = []): Response
{
$files = [
'media' => $path,
];
return $this->httpUpload('cgi-bin/media/upload', $files, $form, compact('type'));
}
/**
* 上传附件资源
* @param string $path
* @param string $mediaType
* @param string $attachmentType
* @return Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function uploadAttachment(string $path, string $mediaType, string $attachmentType): Response
{
$query = [
'media_type' => $mediaType,
'attachment_type' => $attachmentType,
];
return $this->httpUpload('cgi-bin/media/upload_attachment', ['media' => $path], $query);
}
/**
* 获取临时素材
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @param string $mediaId
* @return Response
* @throws TransportExceptionInterface
*/
public function get(string $mediaId): Response
{
return $this->getResources($mediaId, 'cgi-bin/media/get');
}
}

View File

@@ -0,0 +1,225 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 消息发送
* Class MessageClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/10/8
* @package crmeb\services\wechat\client\work
*/
class MessageClient extends BaseClient
{
/**
* Required attributes.
*
* @var array
*/
protected array $required = ['content', 'title', 'url', 'pic_media_id', 'appid', 'page'];
/**
* @var array|string[]
*/
protected array $textMessage = [
'content' => '',
];
/**
* @var array
*/
protected array $imageMessage = [
];
/**
* @var array|string[]
*/
protected array $linkMessage = [
'title' => '',
'picurl' => '',
'desc' => '',
'url' => '',
];
/**
* @var array|string[]
*/
protected array $miniprogramMessage = [
'title' => '',
'pic_media_id' => '',
'appid' => '',
'page' => '',
];
/**
* @param array $message
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function send(array $message): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/message/send', $message);
}
/**
* 添加企业群发消息模板
* @param array $msg
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function submit(array $msg): ResponseInterface|Response
{
$params = $this->formatMessage($msg);
return $this->api->postJson('cgi-bin/externalcontact/add_msg_template', $params);
}
/**
* @param array $data
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
protected function formatMessage(array $data = [])
{
$params = $data;
if (!empty($params['text'])) {
$params['text'] = $this->formatFields($params['text'], $this->textMessage);
}
if (!empty($params['image'])) {
$params['image'] = $this->formatFields($params['image'], $this->imageMessage);
}
if (!empty($params['link'])) {
$params['link'] = $this->formatFields($params['link'], $this->linkMessage);
}
if (!empty($params['miniprogram'])) {
$params['miniprogram'] = $this->formatFields($params['miniprogram'], $this->miniprogramMessage);
}
return $params;
}
/**
* @param array $data
* @param array $default
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
protected function formatFields(array $data = [], array $default = [])
{
$params = array_merge($default, $data);
foreach ($params as $key => $value) {
if (in_array($key, $this->required, true) && empty($value) && empty($default[$key])) {
throw new WechatException(sprintf('Attribute "%s" can not be empty!', $key));
}
$params[$key] = empty($value) ? $default[$key] : $value;
}
return $params;
}
/**
* @param string $welcomeCode
* @param array $msg
* @return ResponseInterface|Response
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
public function sendWelcome(string $welcomeCode, array $msg): ResponseInterface|Response
{
$formattedMsg = $this->formatMessage($msg);
$params = array_merge($formattedMsg, [
'welcome_code' => $welcomeCode,
]);
return $this->api->postJson('cgi-bin/externalcontact/send_welcome_msg', $params);
}
/**
* 获取群发成员发送任务列表
* @param string $msgId 群发消息的id,通过获取群发记录列表接口返回
* @param int|null $limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值
* @param string|null $cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getGroupmsgTask(string $msgId, ?int $limit = null, ?string $cursor = null): ResponseInterface|Response
{
$data = [
'msgid' => $msgId,
'limit' => $limit,
'cursor' => $cursor,
];
$writableData = array_filter($data, function (string $key) use ($data) {
return !is_null($data[$key]);
}, ARRAY_FILTER_USE_KEY);
return $this->api->postJson('cgi-bin/externalcontact/get_groupmsg_task', $writableData);
}
/**
* 获取企业群发成员执行结果
* @param string $msgId 群发消息的id,通过获取群发记录列表接口返回
* @param string $userid 发送成员userid,通过获取群发成员发送任务列表接口返回
* @param int|null $limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值
* @param string|null $cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getGroupmsgSendResult(string $msgId, string $userid, ?int $limit = null, ?string $cursor = null): ResponseInterface|Response
{
$data = [
'msgid' => $msgId,
'userid' => $userid,
'limit' => $limit,
'cursor' => $cursor,
];
$writableData = array_filter($data, function (string $key) use ($data) {
return !is_null($data[$key]);
}, ARRAY_FILTER_USE_KEY);
return $this->api->postJson('cgi-bin/externalcontact/get_groupmsg_send_result', $writableData);
}
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* 朋友圈
* Class MomentClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
* @package crmeb\services\wechat\client\work
*/
class MomentClient extends BaseClient
{
/**
* 创建发表任务
* @param array $param
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function createTask(array $param): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/add_moment_task', $param);
}
/**
* 获取任务创建结果
* @param string $jobId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getTask(string $jobId): ResponseInterface|Response
{
return $this->api->get('cgi-bin/externalcontact/get_moment_task_result', ['jobid' => $jobId]);
}
/**
* 获取企业全部的发表列表
* @param array $params
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function list(array $params): ResponseInterface|Response
{
return $this->api->postJson('cgi-bin/externalcontact/get_moment_list', $params);
}
/**
* 获取客户朋友圈企业发表的列表
* @param string $momentId
* @param string $cursor
* @param int $limit
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getTasks(string $momentId, string $cursor = '', int $limit = 500): ResponseInterface|Response
{
$params = [
'moment_id' => $momentId,
'cursor' => $cursor,
'limit' => $limit
];
return $this->api->postJson('cgi-bin/externalcontact/get_moment_task', $params);
}
/**
* 获取客户朋友圈发表时选择的可见范围
* @param string $momentId
* @param string $userId
* @param string $cursor
* @param int $limit
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getCustomers(string $momentId, string $userId, string $cursor, int $limit): ResponseInterface|Response
{
$params = [
'moment_id' => $momentId,
'userid' => $userId,
'cursor' => $cursor,
'limit' => $limit
];
return $this->api->postJson('cgi-bin/externalcontact/get_moment_customer_list', $params);
}
/**
* 获取客户朋友圈发表后的可见客户列表
* @param string $momentId
* @param string $userId
* @param string $cursor
* @param int $limit
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getSendResult(string $momentId, string $userId, string $cursor, int $limit): ResponseInterface|Response
{
$params = [
'moment_id' => $momentId,
'userid' => $userId,
'cursor' => $cursor,
'limit' => $limit
];
return $this->api->postJson('cgi-bin/externalcontact/get_moment_send_result', $params);
}
/**
* 获取客户朋友圈的互动数据
* @param string $momentId
* @param string $userId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getComments(string $momentId, string $userId): ResponseInterface|Response
{
$params = [
'moment_id' => $momentId,
'userid' => $userId
];
return $this->api->postJson('cgi-bin/externalcontact/get_moment_comments', $params);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\client\work;
use crmeb\services\wechat\client\BaseClient;
use EasyWeChat\Kernel\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* 用户
* Class UserClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\client\work
*/
class UserClient extends BaseClient
{
/**
* 获取部门成员详细信息
* @param int $departmentId
* @param bool $fetchChild
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function getDetailedDepartmentUsers(int $departmentId, bool $fetchChild = false): ResponseInterface|Response
{
$params = [
'department_id' => $departmentId,
'fetch_child' => (int)$fetchChild,
];
return $this->api->get('cgi-bin/user/list', $params);
}
/**
* 获取通讯录成员详情
* @param string $userId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function get(string $userId): ResponseInterface|Response
{
return $this->api->get('cgi-bin/user/get', ['userid' => $userId]);
}
/**
* userid转openid
* @param string $userId
* @param int|null $agentId
* @return Response|ResponseInterface
* @throws TransportExceptionInterface
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public function userIdToOpenid(string $userId, int $agentId = null): ResponseInterface|Response
{
$params = [
'userid' => $userId,
'agentid' => $agentId,
];
return $this->api->postJson('cgi-bin/user/convert_to_openid', $params);
}
}

View File

@@ -0,0 +1,79 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
use crmeb\services\wechat\contract\ConfigHandlerInterface;
use crmeb\services\wechat\contract\ServeConfigInterface;
use crmeb\services\wechat\DefaultConfig;
/**
* Http请求配置
* Class HttpCommonConfig
* @package crmeb\services\wechat\config
*/
class HttpCommonConfig implements ConfigHandlerInterface
{
/**
* @var bool[]
*/
protected array $config = [
'verify' => false,
'timeout' => 5
];
/**
* @var string
*/
protected string $serve;
/**
* @param string $serve
* @return $this
*/
public function setServe(string $serve): static
{
$this->serve = $serve;
return $this;
}
/**
* 获取服务端实例
* @return ServeConfigInterface
*/
public function getServe()
{
return app()->make($this->serve);
}
/**
* 直接获取配置
* @param string $key
* @param null $default
* @return mixed
*/
public function getConfig(string $key, $default = null)
{
if ($value = DefaultConfig::value($key)) {
return $value;
}
return $this->getServe()->getConfig(DefaultConfig::key($key), $default);
}
/**
* @return array
*/
public function all(): array
{
return $this->config;
}
}

View File

@@ -0,0 +1,22 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
/**
* 小微商户
* Class MicroMerchantConfig
* @package crmeb\services\wechat\config
*/
class MicroMerchantConfig
{
}

View File

@@ -0,0 +1,127 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
use crmeb\services\wechat\contract\ConfigHandlerInterface;
use crmeb\services\wechat\DefaultConfig;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Pure;
/**
* 小程序配置
* Class MiniProgramConfig
* @package crmeb\services\wechat\config
*/
class MiniProgramConfig implements ConfigHandlerInterface
{
/**
* APPid
* @var string
*/
public string $appId;
/**
* APPsecret
* @var string
*/
public string $secret;
/**
* Token
* @var string
*/
public string $token;
/**
* EncodingAESKey
* @var string
*/
public string $aesKey;
/**
* @var string
*/
protected string $responseType = 'array';
/**
* http配置
* @var HttpCommonConfig
*/
protected HttpCommonConfig $httpConfig;
/**
* 是否初始化过
* @var bool
*/
protected bool $init = false;
/**
* MiniProgramConfig constructor.
* @param HttpCommonConfig $commonConfig
*/
public function __construct(HttpCommonConfig $commonConfig)
{
$this->httpConfig = $commonConfig;
$this->init();
}
/**
* 初始化
*/
protected function init()
{
if ($this->init) {
return;
}
$this->init = true;
$this->appId = $this->httpConfig->getConfig(DefaultConfig::MINI_APPID, '');
$this->secret = $this->httpConfig->getConfig('mini.secret', '');
$this->token = $this->httpConfig->getConfig('mini.token', '');
$this->aesKey = $this->httpConfig->getConfig('mini.key', '');
}
/**
* 获取配置
* @param string $key
* @param null $default
* @return mixed
*/
public function getConfig(string $key, $default = null)
{
return $this->httpConfig->getConfig($key, $default);
}
/**
* 全部
* @return array
*/
#[ArrayShape([
'app_id' => "string",
'secret' => "string",
'response_type' => "string",
'log' => "array",
'http' => "bool[]"
])]
#[Pure]
public function all(): array
{
return [
'app_id' => $this->appId,
'secret' => $this->secret,
'token' => $this->token,
'aes_key' => $this->aesKey,
'response_type' => $this->responseType,
'http' => $this->httpConfig->all()
];
}
}

View File

@@ -0,0 +1,116 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
use crmeb\services\wechat\contract\ConfigHandlerInterface;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Pure;
/**
* 公众号配置
* Class OfficialAccountConfig
* @package crmeb\services\wechat\config
*/
class OfficialAccountConfig implements ConfigHandlerInterface
{
/**
* AppID
* @var string
*/
public string $appId;
/**
* AppSecret
* @var string
*/
public string $secret;
/**
* Token
* @var string
*/
public string $token;
/**
* EncodingAESKey
* @var string
*/
public string $aesKey;
/**
* 指定 API 调用返回结果的类型
* @var string
*/
protected string $responseType = 'array';
/**
* @var HttpCommonConfig
*/
protected HttpCommonConfig $httpConfig;
/**
* @var bool
*/
protected bool $init = false;
/**
* OfficialAccountConfig constructor.
* @param HttpCommonConfig $commonConfig
*/
public function __construct(HttpCommonConfig $commonConfig)
{
$this->httpConfig = $commonConfig;
$this->init();
}
/**
* 初始化
*/
protected function init()
{
if ($this->init) {
return;
}
$this->init = true;
$this->appId = $this->httpConfig->getConfig('official.appid', '');
$this->secret = $this->httpConfig->getConfig('official.secret', '');
$this->token = $this->httpConfig->getConfig('official.token', '');
$this->aesKey = ($this->httpConfig->getConfig('official.encode', -1) > 0 ? $this->httpConfig->getConfig('official.key', '') : '');
}
/**
* 获取所有配置
* @return array
*/
#[ArrayShape([
'app_id' => "string",
'secret' => "string",
'token' => "string",
'aes_key' => "string",
'response_type' => "string",
'log' => "array",
'http' => "bool[]"
])]
#[Pure]
public function all(): array
{
return [
'app_id' => $this->appId,
'secret' => $this->secret,
'token' => $this->token,
'aes_key' => $this->aesKey,
'response_type' => $this->responseType,
'http' => $this->httpConfig->all()
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
/**
* 开放平台APP配置
* Class OpenAppConfig
* @package crmeb\services\wechat\config
*/
class OpenAppConfig extends OpenWebConfig
{
/**
* OpenAppConfig constructor.
*/
public function init()
{
if ($this->init) {
return;
}
$this->init = true;
$this->appId = $this->config->getConfig('app.appid', '');
$this->secret = $this->config->getConfig('app.secret', '');
$this->token = $this->config->getConfig('app.token', '');
$this->aesKey = $this->config->getConfig('app.key', '');
}
}

View File

@@ -0,0 +1,116 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
use crmeb\services\wechat\contract\ConfigHandlerInterface;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Pure;
/**
* 开放平台网页端配置
* Class OpenWebConfig
* @package crmeb\services\wechat\config
*/
class OpenWebConfig implements ConfigHandlerInterface
{
/**
* Appid
* @var string
*/
public string $appId;
/**
* Appsecret
* @var string
*/
public string $secret;
/**
* @var string
*/
public string $token;
/**
* @var string
*/
public string $aesKey;
/**
* @var bool
*/
protected bool $init = false;
/**
* @var HttpCommonConfig
*/
protected HttpCommonConfig $config;
/**
* OpenWebConfig constructor.
* @param HttpCommonConfig $config
*/
public function __construct(HttpCommonConfig $config)
{
$this->config = $config;
$this->init();
}
/**
* OpenWebConfig constructor.
*/
public function init()
{
if ($this->init) {
return;
}
$this->init = true;
$this->appId = $this->config->getConfig('web.appid', '');
$this->secret = $this->config->getConfig('web.secret', '');
$this->token = $this->config->getConfig('web.token', '');
$this->aesKey = $this->config->getConfig('web.key', '');
}
/**
* 获取配置
* @param string $key
* @param null $default
* @return mixed
*/
public function getConfig(string $key, $default = null)
{
return $this->config->getConfig($key, $default);
}
/**
* @return array
*/
#[ArrayShape([
'app_id' => "string",
'secret' => "string",
'token' => "string",
'aes_key' => "string",
'http' => "bool[]"
])]
#[Pure]
public function all(): array
{
return [
'app_id' => $this->appId,
'secret' => $this->secret,
'token' => $this->token,
'aes_key' => $this->aesKey,
'http' => $this->config->all()
];
}
}

View File

@@ -0,0 +1,159 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
use crmeb\services\wechat\contract\ConfigHandlerInterface;
use crmeb\services\wechat\DefaultConfig;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Pure;
/**
* 支付配置
* Class PaymentConfig
* @package crmeb\services\wechat\config
*/
class PaymentConfig implements ConfigHandlerInterface
{
/**
* appid
* @var string
*/
public string $appId;
/**
* 商户密钥
* @var string
*/
public string $mchId;
/**
* 小程序商户号
* @var string
*/
public string $routineMchId;
/**
* API密钥
* @var string
*/
public string $key;
/**
* 证书cert
* @var string
*/
public string $certPath;
/**
* 证书key
* @var string
*/
public string $keyPath;
/**
* 支付异步回调地址
* @var string
*/
public string $notifyUrl;
/**
* 退款异步通知
* @var string
*/
public string $refundUrl;
/**
* @var HttpCommonConfig
*/
protected HttpCommonConfig $httpConfig;
/**
* @var bool
*/
protected bool $init = false;
/**
* PaymentConfig constructor.
* @param HttpCommonConfig $commonConfig
*/
public function __construct(HttpCommonConfig $commonConfig)
{
$this->httpConfig = $commonConfig;
$this->init();
}
/**
* 初始化
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
*/
protected function init()
{
if ($this->init) {
return;
}
$this->init = true;
$this->appId = $this->httpConfig->getConfig(DefaultConfig::OFFICIAL_APPID, '');
$this->mchId = $this->httpConfig->getConfig(DefaultConfig::PAY_MCHID, '');
$this->routineMchId = $this->httpConfig->getConfig('pay.routine_mchid', '');
$this->key = $this->httpConfig->getConfig('pay.key', '');
$certPath = env('RECEPTACLE_ENABLE', false) ? env('RECEPTACLE_PAYCERT', '') : $this->httpConfig->getConfig('pay.client_cert', '');
$keyPath = env('RECEPTACLE_ENABLE', false) ? env('RECEPTACLE_PAYKEY', '') : $this->httpConfig->getConfig('pay.client_key', '');
$this->certPath = str_replace('//', '/', public_path() . $certPath);
$this->keyPath = str_replace('//', '/', public_path() . $keyPath);
$this->notifyUrl = trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.notifyUrl');
$this->refundUrl = trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.refundUrl');
}
/**
* 获取配置
* @param string $key
* @param null $default
* @return mixed
*/
public function getConfig(string $key, $default = null)
{
return $this->httpConfig->getConfig($key, $default);
}
/**
* 全部配置
* @return array
*/
#[Pure]
#[ArrayShape([
'app_id' => "string",
'mch_id' => "string",
'v2_secret_key' => "string",
'private_key' => "string",
'certificate' => "string",
'notify_url' => "string",
'log' => "array",
'http' => "bool[]"
])]
public function all(): array
{
return [
'app_id' => $this->appId,
'mch_id' => $this->mchId,
'v2_secret_key' => $this->key,
'private_key' => $this->keyPath,
'certificate' => $this->certPath,
'notify_url' => $this->notifyUrl,
'http' => $this->httpConfig->all()
];
}
}

View File

@@ -0,0 +1,201 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\config;
use crmeb\services\wechat\contract\ConfigHandlerInterface;
use crmeb\services\wechat\DefaultConfig;
/**
* Class V3PaymentConfig
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/30
* @package crmeb\services\wechat\config
*/
class V3PaymentConfig implements ConfigHandlerInterface
{
/**
* appid
* @var string
*/
public string $appId;
/**
* 商户密钥
* @var string
*/
public string $mchId;
/**
* API密钥
* @var string
*/
public string $key;
/**
* 证书序列号
* @var string
*/
public string $serialNo;
/**
* 证书cert
* @var string
*/
public string $certPath;
/**
* 证书key
* @var string
*/
public string $keyPath;
/**
* 支付异步回调地址
* @var string
*/
public string $notifyUrl;
/**
* 退款异步通知
* @var string
*/
public string $refundUrl;
/**
* 小程序appid
* @var string
*/
public string $miniprogAppid;
/**
* 应用appid
* @var string
*/
public string $appAppid;
/**
* 微信公众号appid
* @var string
*/
public string $wechatAppid;
/**
* v3支付公钥
* @var string
*/
public string $publicKey;
/**
* v3支付公钥证书
* @var string
*/
public string $publicPem;
/**
* web appid
* @var string
*/
public string $webAppid;
/**
* 是否v3支付
* @var bool
*/
public bool $isV3PAy = true;
/**
* @var HttpCommonConfig
*/
protected HttpCommonConfig $httpConfig;
/**
* @var bool
*/
protected bool $init = false;
/**
* V3PaymentConfig constructor.
*/
public function __construct()
{
$this->httpConfig = app()->make(HttpCommonConfig::class);
$this->init();
}
/**
* 初始化
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
protected function init()
{
if ($this->init) {
return;
}
$this->init = true;
$this->appId = $this->httpConfig->getConfig(DefaultConfig::OFFICIAL_APPID, '');
$this->mchId = $this->httpConfig->getConfig(DefaultConfig::PAY_MCHID, '');
$this->serialNo = $this->httpConfig->getConfig('v3_pay.serial_no', '');
$this->key = $this->httpConfig->getConfig('v3_pay.key', '');
$this->isV3PAy = !!$this->httpConfig->getConfig('v3_pay.pay_type', false);
$this->certPath = str_replace('//', '/', public_path() . $this->httpConfig->getConfig('pay.client_cert', ''));
$this->keyPath = str_replace('//', '/', public_path() . $this->httpConfig->getConfig('pay.client_key', ''));
$this->notifyUrl = trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.notifyUrl');
$this->refundUrl = trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.refundUrl');
$this->appAppid = $this->httpConfig->getConfig(DefaultConfig::APP_APPID, '');
$this->webAppid = $this->httpConfig->getConfig(DefaultConfig::WEB_APPID, '');
$this->wechatAppid = $this->httpConfig->getConfig(DefaultConfig::OFFICIAL_APPID, '');
$this->miniprogAppid = $this->httpConfig->getConfig(DefaultConfig::MINI_APPID, '');
$this->publicKey = $this->httpConfig->getConfig('v3_pay.public_key', '');
$this->publicPem = str_replace('//', '/', public_path() . $this->httpConfig->getConfig('v3_pay.public_pem', ''));
}
/**
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/30
*/
public function all(): array
{
return [
'app_id' => $this->appId,
'serial_no' => $this->serialNo,
'mch_id' => $this->mchId,
'key' => $this->key,
'cert_path' => $this->certPath,
'key_path' => $this->keyPath,
'notify_url' => $this->notifyUrl,
'http' => $this->httpConfig->all(),
'other' => [
'wechat' => [
'appid' => $this->wechatAppid,
],
'web' => [
'appid' => $this->webAppid,
],
'app' => [
'appid' => $this->appAppid,
],
'miniprog' => [
'appid' => $this->miniprogAppid,
]
],
];
}
}

View File

@@ -0,0 +1,162 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\config;
use crmeb\services\wechat\contract\ConfigHandlerInterface;
use crmeb\services\wechat\contract\WorkAppConfigHandlerInterface;
use crmeb\services\wechat\DefaultConfig;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Pure;
/**
* 企业微信配置
* Class WorkConfig
* @package crmeb\services\wechat\config
*/
class WorkConfig implements ConfigHandlerInterface
{
//应用
const TYPE_APP = 'app';
//客户联系
const TYPE_USER = 'user';
//通讯录同步
const TYPE_ADDRESS = 'address';
//客服
const TYPE_KEFU = 'kefu';
//审批
const TYPE_APPROVE = 'approve';
//会议室
const TYPE_MEETING = 'meeting';
//自建应用
const TYPE_USER_APP = 'build';
/**
* @var string
*/
public string $corpId;
/**
* @var string
*/
public string $token;
/**
* @var string
*/
public string $aesKey;
/**
* @var string
*/
protected string $responseType = 'array';
/**
* @var HttpCommonConfig
*/
protected HttpCommonConfig $httpConfig;
/**
* @var bool
*/
protected bool $init = false;
/**
* @var string
*/
protected string $handler;
/**
* @var array
*/
protected array $appConfig;
/**
* WorkConfig constructor.
* @param HttpCommonConfig $commonConfig
*/
public function __construct(HttpCommonConfig $commonConfig)
{
$this->httpConfig = $commonConfig;
$this->init();
}
/**
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
*/
protected function init()
{
if ($this->init) {
return;
}
$this->init = true;
$this->corpId = $this->httpConfig->getConfig(DefaultConfig::WORK_CORP_ID, '');
$this->token = $this->httpConfig->getConfig('work.token', '');
$this->aesKey = $this->httpConfig->getConfig('work.key', '');
}
/**
* 获取全部值
* @return array
*/
#[ArrayShape([
'corp_id' => "string",
'token' => "string",
'aes_key' => "string",
'response_type' => "string",
'log' => "array",
'http' => "bool[]"
])]
#[Pure]
public function all(): array
{
return [
'corp_id' => $this->corpId,
'token' => $this->token,
'aes_key' => $this->aesKey,
'response_type' => $this->responseType,
'http' => $this->httpConfig->all()
];
}
/**
* 获取应用配置
* @param string $type
* @return array
*/
public function getAppConfig(string $type): array
{
if (!isset($this->appConfig[$type])) {
/** @var WorkAppConfigHandlerInterface $make */
$make = app()->make($this->handler);
if (!$this->corpId) {
$this->init();
}
$this->appConfig[$type] = $make->getAppConfig($this->corpId, $type);
}
return $this->appConfig[$type];
}
/**
* 设置
* @param string $handler
* @return $this
*/
public function setHandler(string $handler): self
{
$this->handler = $handler;
return $this;
}
}

View File

@@ -0,0 +1,41 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\contract;
use EasyWeChat\Pay\Contracts\Application;
use EasyWeChat\MiniApp\Contracts\Application as MiniAppApplication;
use EasyWeChat\OfficialAccount\Contracts\Application as ApplicationInterface;
use EasyWeChat\Work\Contracts\Application as WorkApplication;
/**
* Interface BaseApplicationInterface
* @package crmeb\services\wechat\contract
*/
interface BaseApplicationInterface
{
/**
* @return static
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public static function instance(): static;
/**
* @return ApplicationInterface|Application|MiniAppApplication|WorkApplication
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public function application(): ApplicationInterface|Application|MiniAppApplication|WorkApplication;
}

View File

@@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\contract;
/**
* 配置
* Interface ConfigHandlerInterface
* @package crmeb\services\wechat\contract
*/
interface ConfigHandlerInterface
{
/**
* 获取全部
* @return array
*/
public function all(): array;
}

View File

@@ -0,0 +1,28 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\contract;
/**
* Interface ServeConfigInterface
* @package crmeb\services\wechat\contract
*/
interface ServeConfigInterface
{
/**
* @param string $key
* @param null $default
* @return mixed
*/
public function getConfig(string $key, $default = null);
}

View File

@@ -0,0 +1,30 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\wechat\contract;
/**
* 企业微信获取应用配置
* Interface WorkAppConfigHandlerInterface
* @package crmeb\services\wechat\contract
*/
interface WorkAppConfigHandlerInterface
{
/**
* 获取应用配置
* @param string $corpId
* @param string $type 应用标识
* @return array
*/
public function getAppConfig(string $corpId, string $type): array;
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
class Article extends Message
{
/**
* @var string
*/
protected string $type = 'mpnews';
/**
* Properties.
*
* @var array
*/
protected array $properties = [
'thumb_media_id',
'author',
'title',
'content',
'digest',
'source_url',
'show_cover',
];
/**
* Aliases of attribute.
*
* @var array
*/
protected array $jsonAliases = [
'content_source_url' => 'source_url',
'show_cover_pic' => 'show_cover',
];
/**
* @var array
*/
protected array $required = [
'thumb_media_id',
'title',
'content',
'show_cover',
];
}

View File

@@ -0,0 +1,249 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
use crmeb\services\wechat\WechatException;
use EasyWeChat\Kernel\Support\Arr;
use think\helper\Str;
trait HasAttributes
{
/**
* @var array
*/
protected array $attributes = [];
/**
* @var bool
*/
protected bool $snakeable = true;
/**
* Set Attributes.
*
* @param array $attributes
*
* @return HasAttributes
*/
public function setAttributes(array $attributes = []): self
{
$this->attributes = $attributes;
return $this;
}
/**
* Set attribute.
*
* @param string $attribute
* @param string $value
*
* @return $this
*/
public function setAttribute(string $attribute, string $value)
{
Arr::set($this->attributes, $attribute, $value);
return $this;
}
/**
* Get attribute.
*
* @param string $attribute
* @param mixed|null $default
*
* @return mixed
*/
public function getAttribute(string $attribute, mixed $default = null)
{
return Arr::get($this->attributes, $attribute, $default);
}
/**
* @param string $attribute
*
* @return bool
*/
public function isRequired(string $attribute)
{
return in_array($attribute, $this->getRequired(), true);
}
/**
* @return array|mixed
*/
public function getRequired(): mixed
{
return property_exists($this, 'required') ? $this->required : [];
}
/**
* Set attribute.
*
* @param string $attribute
* @param mixed $value
*
* @return $this
*/
public function with(string $attribute, mixed $value)
{
$this->snakeable && $attribute = Str::snake($attribute);
$this->setAttribute($attribute, $value);
return $this;
}
/**
* Override parent set() method.
*
* @param string $attribute
* @param mixed $value
*
* @return $this
*/
public function set(string $attribute, mixed $value)
{
$this->setAttribute($attribute, $value);
return $this;
}
/**
* Override parent get() method.
*
* @param string $attribute
* @param mixed|null $default
*
* @return mixed
*/
public function get(string $attribute, mixed $default = null)
{
return $this->getAttribute($attribute, $default);
}
/**
* @param string $key
*
* @return bool
*/
public function has(string $key)
{
return Arr::has($this->attributes, $key);
}
/**
* @param array $attributes
*
* @return $this
*/
public function merge(array $attributes)
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
/**
* @param array|string $keys
*
* @return array
*/
public function only(array|string $keys)
{
return array_intersect_key($this->attributes, array_flip((array)$keys));
}
/**
* Return all items.
*
* @return array
*
*/
public function all()
{
$this->checkRequiredAttributes();
return $this->attributes;
}
/**
* Magic call.
*
* @param string $method
* @param array $args
*
* @return $this
*/
public function __call($method, $args)
{
if (0 === stripos($method, 'with')) {
return $this->with(substr($method, 4), array_shift($args));
}
throw new \BadMethodCallException(sprintf('Method "%s" does not exists.', $method));
}
/**
* Magic get.
*
* @param string $property
*
* @return mixed
*/
public function __get($property)
{
return $this->get($property);
}
/**
* Magic set.
*
* @param string $property
* @param mixed $value
*
* @return $this
*/
public function __set($property, $value)
{
return $this->with($property, $value);
}
/**
* Whether or not an data exists by key.
*
* @param string $key
*
* @return bool
*/
public function __isset($key)
{
return isset($this->attributes[$key]);
}
/**
* Check required attributes.
*
*/
protected function checkRequiredAttributes()
{
foreach ($this->getRequired() as $attribute) {
if (is_null($this->get($attribute))) {
throw new WechatException(sprintf('"%s" cannot be empty.', $attribute));
}
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
/**
* 图片
* Class Image
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
* @package crmeb\services\wechat\message
*/
class Image extends Media
{
/**
* 消息类型
* @var string
*/
protected string $type = 'image';
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
use think\helper\Str;
class Media extends Message
{
/**
* Properties.
*
* @var array
*/
protected array $properties = ['media_id'];
/**
* @var array
*/
protected array $required = [
'media_id',
];
/**
* MaterialClient constructor.
*
* @param string $mediaId
* @param string|null $type
* @param array $attributes
*/
public function __construct(string $mediaId, string $type = null, array $attributes = [])
{
parent::__construct(array_merge(['media_id' => $mediaId], $attributes));
!empty($type) && $this->setType($type);
}
/**
* @return string
*
*/
public function getMediaId(): string
{
$this->checkRequiredAttributes();
return $this->get('media_id');
}
public function toXmlArray()
{
return [
Str::studly($this->getType()) => [
'MediaId' => $this->get('media_id'),
],
];
}
}

View File

@@ -0,0 +1,217 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Support\XML;
/**
* 消息
* Class Message
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
* @package crmeb\services\wechat\message
*/
class Message implements MessageInterface
{
use HasAttributes;
public const TEXT = 2;
public const IMAGE = 4;
public const VOICE = 8;
public const VIDEO = 16;
public const SHORT_VIDEO = 32;
public const LOCATION = 64;
public const LINK = 128;
public const DEVICE_EVENT = 256;
public const DEVICE_TEXT = 512;
public const FILE = 1024;
public const TEXT_CARD = 2048;
public const TRANSFER = 4096;
public const EVENT = 1048576;
public const MINIPROGRAM_PAGE = 2097152;
public const MINIPROGRAM_NOTICE = 4194304;
public const ALL = self::TEXT | self::IMAGE | self::VOICE | self::VIDEO | self::SHORT_VIDEO | self::LOCATION | self::LINK
| self::DEVICE_EVENT | self::DEVICE_TEXT | self::FILE | self::TEXT_CARD | self::TRANSFER | self::EVENT
| self::MINIPROGRAM_PAGE | self::MINIPROGRAM_NOTICE;
/**
* @var string
*/
protected string $type;
/**
* @var int
*/
protected int $id;
/**
* @var string
*/
protected string $to;
/**
* @var string
*/
protected string $from;
/**
* @var array
*/
protected array $properties = [];
/**
* @var array
*/
protected array $jsonAliases = [];
/**
* Message constructor.
*
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
$this->setAttributes($attributes);
}
/**
* Return type name message.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @param string $type
*/
public function setType(string $type)
{
$this->type = $type;
}
/**
* Magic getter.
*
* @param string $property
*
* @return mixed
*/
public function __get($property)
{
if (property_exists($this, $property)) {
return $this->$property;
}
return $this->getAttribute($property);
}
/**
* Magic setter.
*
* @param string $property
* @param mixed $value
*
* @return Message
*/
public function __set($property, $value)
{
if (property_exists($this, $property)) {
$this->$property = $value;
} else {
$this->setAttribute($property, $value);
}
return $this;
}
/**
* @param array $appends
*
* @return array
*/
public function transformForJsonRequestWithoutType(array $appends = [])
{
return $this->transformForJsonRequest($appends, false);
}
/**
* @param array $appends
* @param bool $withType
*
* @return array
*
* @throws InvalidArgumentException
*/
public function transformForJsonRequest(array $appends = [], bool $withType = true): array
{
if (!$withType) {
return $this->propertiesToArray([], $this->jsonAliases);
}
$messageType = $this->getType();
$data = array_merge(['msgtype' => $messageType], $appends);
$data[$messageType] = array_merge($data[$messageType] ?? [], $this->propertiesToArray([], $this->jsonAliases));
return $data;
}
/**
* @param array $appends
* @param bool $returnAsArray
*
* @return string
*/
public function transformToXml(array $appends = [], bool $returnAsArray = false)
{
$data = array_merge(['MsgType' => $this->getType()], $this->toXmlArray(), $appends);
return $returnAsArray ? $data : XML::build($data);
}
/**
* @param array $data
* @param array $aliases
*
* @return array
*
* @throws InvalidArgumentException
*/
protected function propertiesToArray(array $data, array $aliases = []): array
{
$this->checkRequiredAttributes();
foreach ($this->attributes as $property => $value) {
if (is_null($value) && !$this->isRequired($property)) {
continue;
}
$alias = array_search($property, $aliases, true);
$data[$alias ?: $property] = $this->get($property);
}
return $data;
}
public function toXmlArray()
{
throw new \RuntimeException(sprintf('Class "%s" cannot support transform to XML message.', __CLASS__));
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
interface MessageInterface
{
/**
* @return string
*/
public function getType(): string;
/**
* @param array $appends
* @param bool $withType
* @return array
*/
public function transformForJsonRequest(array $appends = [], bool $withType = true): array;
/**
* @return string|array
*/
public function transformToXml();
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
class News extends Message
{
/**
* @var string
*/
protected string $type = 'news';
/**
* @var array
*/
protected array $properties = [
'items',
];
/**
* News constructor.
*
* @param array $items
*/
public function __construct(array $items = [])
{
parent::__construct(compact('items'));
}
/**
* @param array $data
* @param array $aliases
*
* @return array
*/
public function propertiesToArray(array $data, array $aliases = []): array
{
return ['articles' => array_map(function ($item) {
if ($item instanceof NewsItem) {
return $item->toJsonArray();
}
}, $this->get('items'))];
}
/**
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
public function toXmlArray(): array
{
$items = [];
foreach ($this->get('items') as $item) {
if ($item instanceof NewsItem) {
$items[] = $item->toXmlArray();
}
}
return [
'ArticleCount' => count($items),
'Articles' => $items,
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
/**
* Class NewsItem
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
* @package crmeb\services\wechat\message
*/
class NewsItem extends Message
{
/**
* Messages type.
*
* @var string
*/
protected string $type = 'news';
/**
* Properties.
*
* @var array
*/
protected array $properties = [
'title',
'description',
'url',
'image',
];
/**
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
public function toJsonArray(): array
{
return [
'title' => $this->get('title'),
'description' => $this->get('description'),
'url' => $this->get('url'),
'picurl' => $this->get('image'),
];
}
/**
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
public function toXmlArray(): array
{
return [
'Title' => $this->get('title'),
'Description' => $this->get('description'),
'Url' => $this->get('url'),
'PicUrl' => $this->get('image'),
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
/**
* Class Raw
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
* @package crmeb\services\wechat\message
*/
class Raw extends Message
{
/**
* @var string
*/
protected string $type = 'raw';
/**
* Properties.
*
* @var array
*/
protected array $properties = ['content'];
/**
* Constructor.
*
* @param string $content
*/
public function __construct(string $content)
{
parent::__construct(['content' => strval($content)]);
}
/**
* @param array $appends
* @param bool $withType
*
* @return array
*/
public function transformForJsonRequest(array $appends = [], bool $withType = true): array
{
return json_decode($this->content, true) ?? [];
}
public function __toString()
{
return $this->get('content') ?? '';
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
/**
* text消息
* Class Text
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
* @package crmeb\services\wechat\message
*/
class Text extends Message
{
/**
* Message type.
*
* @var string
*/
protected string $type = 'text';
/**
* Properties.
*
* @var array
*/
protected array $properties = ['content'];
/**
* Text constructor.
*
* @param string $content
*/
public function __construct(string $content = '')
{
parent::__construct(compact('content'));
}
/**
* @return array
*/
public function toXmlArray(): array
{
return [
'Content' => $this->get('content'),
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
/**
* 卡卷
* Class TextCard
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
* @package crmeb\services\wechat\message
*/
class TextCard extends Message
{
/**
* Messages type.
*
* @var string
*/
protected string $type = 'textcard';
/**
* Properties.
*
* @var array
*/
protected array $properties = [
'title',
'description',
'url',
];
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
class Transfer extends Message
{
/**
* Messages type.
*
* @var string
*/
protected string $type = 'transfer_customer_service';
/**
* Properties.
*
* @var array
*/
protected array $properties = [
'account',
];
/**
* Transfer constructor.
*
* @param string|null $account
*/
public function __construct(string $account = null)
{
parent::__construct(compact('account'));
}
/**
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
public function toXmlArray(): array
{
return empty($this->get('account')) ? [] : [
'TransInfo' => [
'KfAccount' => $this->get('account'),
],
];
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
class Video extends Media
{
/**
**
* Messages type.
*
* @var string
*/
protected string $type = 'video';
/**
* Properties.
*
* @var array
*/
protected array $properties = [
'title',
'description',
'media_id',
'thumb_media_id',
];
/**
* Video constructor.
*
* @param string $mediaId
* @param array $attributes
*/
public function __construct(string $mediaId, array $attributes = [])
{
parent::__construct($mediaId, 'video', $attributes);
}
/**
* @return array[]
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/26
*/
public function toXmlArray(): array
{
return [
'Video' => [
'MediaId' => $this->get('media_id'),
'Title' => $this->get('title'),
'Description' => $this->get('description'),
],
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\message;
class Voice extends Media
{
/**
* Messages type.
*
* @var string
*/
protected string $type = 'voice';
/**
* Properties.
*
* @var array
*/
protected array $properties = [
'media_id',
'recognition',
];
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\util;
use InvalidArgumentException;
/**
* AES 加密解密
* Class AES
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\util
*/
class AES
{
/**
* 加密
* @param string $text
* @param string $key
* @param string $iv
* @param int $option
*
* @return string
*/
public static function encrypt(string $text, string $key, string $iv, int $option = OPENSSL_RAW_DATA): string
{
self::validateKey($key);
self::validateIv($iv);
return openssl_encrypt($text, self::getMode($key), $key, $option, $iv);
}
/**
* 解密
* @param string $cipherText
* @param string $key
* @param string $iv
* @param int $option
* @param string|null $method
*
* @return string
*/
public static function decrypt(string $cipherText, string $key, string $iv, int $option = OPENSSL_RAW_DATA, $method = null): string
{
self::validateKey($key);
self::validateIv($iv);
return openssl_decrypt($cipherText, $method ?: self::getMode($key), $key, $option, $iv);
}
/**
* @param string $key
*
* @return string
*/
public static function getMode(string $key)
{
return 'aes-' . (8 * strlen($key)) . '-cbc';
}
/**
* @param string $key
*/
public static function validateKey(string $key)
{
if (!in_array(strlen($key), [16, 24, 32], true)) {
throw new InvalidArgumentException(sprintf('Key length must be 16, 24, or 32 bytes; got key len (%s).', strlen($key)));
}
}
/**
* @param string $iv
*
* @throws InvalidArgumentException
*/
public static function validateIv(string $iv)
{
if (!empty($iv) && 16 !== strlen($iv)) {
throw new InvalidArgumentException('IV length must be 16 bytes.');
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\util;
use Closure;
/**
* 帮助类
* Class Helper
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
* @package crmeb\services\wechat\util
*/
class Helper
{
/**
* 获取加密方式
* @param string $signType
* @param string $secretKey
* @return Closure|string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/18
*/
public static function getEncryptMethod(string $signType, string $secretKey = ''): string|Closure
{
if ('HMAC-SHA256' === $signType) {
return function ($str) use ($secretKey) {
return hash_hmac('sha256', $str, $secretKey);
};
}
return 'md5';
}
/**
* 生成签名
* @param array $attributes
* @param string $key
* @param string $encryptMethod
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/14
*/
public static function generateSign(array $attributes, string $key, string $encryptMethod = 'md5'): string
{
ksort($attributes);
$attributes['key'] = $key;
return strtoupper(call_user_func_array($encryptMethod, [urldecode(http_build_query($attributes))]));
}
/**
* 截取数据
* @param string $tel
* @return array|string|string[]
* @author 等风来
* @email 136327134@qq.com
* @date 2023/9/15
*/
public static function encryptTel(string $tel)
{
return substr_replace($tel, '****', 3, 4);
}
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\util;
class XML
{
/**
* XML to array.
*
* @param string $xml XML string
*
* @return array
*/
public static function parse(string $xml)
{
$backup = PHP_MAJOR_VERSION < 8 ? libxml_disable_entity_loader(true) : null;
$result = self::normalize(simplexml_load_string(self::sanitize($xml), 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_NOCDATA | LIBXML_NOBLANKS));
PHP_MAJOR_VERSION < 8 && libxml_disable_entity_loader($backup);
return $result;
}
/**
* XML encode.
*
* @param mixed $data
* @param string $root
* @param string $item
* @param string $attr
* @param string $id
*
* @return string
*/
public static function build(
array $data,
string $root = 'xml',
string $item = 'item',
string $attr = '',
string $id = 'id'
): string
{
if (is_array($attr)) {
$_attr = [];
foreach ($attr as $key => $value) {
$_attr[] = "{$key}=\"{$value}\"";
}
$attr = implode(' ', $_attr);
}
$attr = trim($attr);
$attr = empty($attr) ? '' : " {$attr}";
$xml = "<{$root}{$attr}>";
$xml .= self::data2Xml($data, $item, $id);
$xml .= "</{$root}>";
return $xml;
}
/**
* Build CDATA.
*
* @param string $string
*
* @return string
*/
public static function cdata(string $string)
{
return sprintf('<![CDATA[%s]]>', $string);
}
/**
* Object to array.
*
*
* @param mixed $obj
*
* @return array
*/
protected static function normalize(mixed $obj)
{
$result = null;
if (is_object($obj)) {
$obj = (array)$obj;
}
if (is_array($obj)) {
foreach ($obj as $key => $value) {
$res = self::normalize($value);
if (('@attributes' === $key) && ($key)) {
$result = $res; // @codeCoverageIgnore
} else {
$result[$key] = $res;
}
}
} else {
$result = $obj;
}
return $result;
}
/**
* Array to XML.
*
* @param array $data
* @param string $item
* @param string $id
*
* @return string
*/
protected static function data2Xml(array $data, string $item = 'item', string $id = 'id')
{
$xml = $attr = '';
foreach ($data as $key => $val) {
if (is_numeric($key)) {
$id && $attr = " {$id}=\"{$key}\"";
$key = $item;
}
$xml .= "<{$key}{$attr}>";
if ((is_array($val) || is_object($val))) {
$xml .= self::data2Xml((array)$val, $item, $id);
} else {
$xml .= is_numeric($val) ? $val : self::cdata($val);
}
$xml .= "</{$key}>";
}
return $xml;
}
/**
* Delete invalid characters in XML.
*
* @see https://www.w3.org/TR/2008/REC-xml-20081126/#charsets - XML charset range
* @see http://php.net/manual/en/regexp.reference.escape.php - escape in UTF-8 mode
*
* @param string $xml
*
* @return string
*/
public static function sanitize(string $xml)
{
return preg_replace('/[^\x{9}\x{A}\x{D}\x{20}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]+/u', '', $xml);
}
}

View File

@@ -0,0 +1,281 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\v3pay;
use crmeb\exceptions\PayException;
use crmeb\services\wechat\config\V3PaymentConfig;
use crmeb\services\wechat\WechatException;
use think\exception\InvalidArgumentException;
/**
* Class BaseClient
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/30
* @package crmeb\services\wechat\v3pay
*/
class BaseClient
{
use Certficates;
const BASE_URL = 'https://api.mch.weixin.qq.com/';
const AUTH_TAG_LENGTH_BYTE = 16;
/**
* BaseClient constructor.
* @param V3PaymentConfig $config
*/
public function __construct(protected V3PaymentConfig $config)
{
}
/**
* request.
*
* @param string $endpoint
* @param string $method
* @param array $options
* @param bool $serial
* @return mixed
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function request(string $endpoint, string $method = 'POST', array $options = [], bool $serial = true)
{
$body = $options['body'] ?? '';
if (isset($options['json'])) {
$body = json_encode($options['json']);
$options['body'] = $body;
unset($options['json']);
}
$headers = [
'Content-Type' => 'application/json',
'User-Agent' => 'curl',
'Accept' => 'application/json',
'Authorization' => $this->getAuthorization($endpoint, $method, $body),
];
$options['headers'] = array_merge($headers, ($options['headers'] ?? []));
if ($serial) {
if ($this->config->publicKey != '') {
$options['headers']['Wechatpay-Serial'] = $this->config->publicKey;
} else {
$options['headers']['Wechatpay-Serial'] = $this->getCertficatescAttr('serial_no');
}
}
return $this->_doRequestCurl($method, self::BASE_URL . $endpoint, $options);
}
/**
* @param string $method
* @param string $location
* @param array $options
* @return mixed
*/
private function _doRequestCurl(string $method, string $location, array $options = [])
{
$curl = curl_init();
// POST数据设置
if (strtolower($method) === 'post') {
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data'] ?? $options['body'] ?? '');
}
// CURL头信息设置
if (!empty($options['headers'])) {
$headers = [];
foreach ($options['headers'] as $k => $v) {
$headers[] = "$k: $v";
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
}
curl_setopt($curl, CURLOPT_URL, $location);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 60);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
$content = curl_exec($curl);
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
curl_close($curl);
return json_decode(substr($content, $headerSize), true);
}
/**
* To id card, mobile phone number and other fields sensitive information encryption.
*
* @param string $string
*
* @return string
*/
protected function encryptSensitiveInformation(string $string)
{
$certificates = $this->config->certPath;
if (null === $certificates) {
throw new WechatException('config certificate connot be empty.');
}
$encrypted = '';
if (openssl_public_encrypt($string, $encrypted, $certificates, OPENSSL_PKCS1_OAEP_PADDING)) {
//base64编码
$sign = base64_encode($encrypted);
} else {
throw new WechatException('Encryption of sensitive information failed');
}
return $sign;
}
/**
* @param string $url
* @param string $method
* @param string $body
* @return string
*/
protected function getAuthorization(string $url, string $method, string $body)
{
$nonceStr = uniqid();
$timestamp = time();
$message = $method . "\n" .
'/' . $url . "\n" .
$timestamp . "\n" .
$nonceStr . "\n" .
$body . "\n";
openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
$schema = 'WECHATPAY2-SHA256-RSA2048 ';
$token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$this->config->mchId, $nonceStr, $timestamp, $this->config->serialNo, $sign);
return $schema . $token;
}
/**
* 获取商户私钥
* @return bool|resource
*/
protected function getPrivateKey()
{
$key_path = $this->config->keyPath;
if (!file_exists($key_path)) {
throw new \InvalidArgumentException(
"SSL certificate not found: {$key_path}"
);
}
return openssl_pkey_get_private(file_get_contents($key_path));
}
/**
* 获取商户公钥
* @return bool|resource
*/
protected function getPublicKey()
{
if ($this->config->publicKey != '') {
$key_path = $this->config->publicPem;
} else {
$key_path = $this->config->certPath;
}
if (!file_exists($key_path)) {
throw new \InvalidArgumentException(
"SSL certificate not found: {$key_path}"
);
}
return openssl_pkey_get_public(file_get_contents($key_path));
}
/**
* 替换url
* @param string $url
* @param $search
* @param $replace
* @return array|string|string[]
*/
public function getApiUrl(string $url, $search, $replace)
{
$newSearch = [];
foreach ($search as $key) {
$newSearch[] = '{' . $key . '}';
}
return str_replace($newSearch, $replace, $url);
}
/**
* @param int $padding
*/
private static function paddingModeLimitedCheck(int $padding): void
{
if (!($padding === OPENSSL_PKCS1_OAEP_PADDING || $padding === OPENSSL_PKCS1_PADDING)) {
throw new PayException(sprintf("Doesn't supported padding mode(%d), here only support OPENSSL_PKCS1_OAEP_PADDING or OPENSSL_PKCS1_PADDING.", $padding));
}
}
/**
* 加密数据
* @param string $plaintext
* @param int $padding
* @return string
*/
public function encryptor(string $plaintext, int $padding = OPENSSL_PKCS1_OAEP_PADDING)
{
self::paddingModeLimitedCheck($padding);
if (!openssl_public_encrypt($plaintext, $encrypted, $this->getPublicKey(), $padding)) {
throw new PayException('Encrypting the input $plaintext failed, please checking your $publicKey whether or nor correct.');
}
return base64_encode($encrypted);
}
/**
* decrypt ciphertext.
*
* @param array $encryptCertificate
*
* @return string
*/
public function decrypt(array $encryptCertificate)
{
$ciphertext = base64_decode($encryptCertificate['ciphertext'], true);
$associatedData = $encryptCertificate['associated_data'];
$nonceStr = $encryptCertificate['nonce'];
$aesKey = $this->config->key;
try {
// ext-sodium (default installed on >= PHP 7.2)
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// ext-libsodium (need install libsodium-php 1.x via pecl)
if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// openssl (PHP >= 7.1 support AEAD)
if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
$ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
$authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
}
} catch (\Exception $exception) {
throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
} catch (\SodiumException $exception) {
throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
}
throw new InvalidArgumentException('AEAD_AES_256_GCM 需要 PHP 7.1 以上或者安装 libsodium-php');
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\v3pay;
use crmeb\exceptions\PayException;
use crmeb\services\wechat\config\V3PaymentConfig;
use Psr\SimpleCache\InvalidArgumentException;
use think\facade\Cache;
/**
* Class Certficates
* @package crmeb\services\easywechat\v3pay
* @property V3PaymentConfig $config
*/
trait Certficates
{
/**
* @param string|null $key
* @return array|mixed|null
* @throws InvalidArgumentException
*/
public function getCertficatescAttr(string $key = null)
{
$driver = Cache::store('file');
$cacheKey = '_wx_v3' . $this->config->serialNo;
if ($driver->has($cacheKey)) {
$res = $driver->get($cacheKey);
if ($key && $res) {
return $res[$key] ?? null;
} else {
return $res;
}
}
$certficates = $this->getCertficates();
$driver->set($cacheKey, $certficates, 3600 * 24 * 30);
if ($key && $certficates) {
return $certficates[$key] ?? null;
}
return $certficates;
}
/**
* get certficates.
*
* @return array
*/
public function getCertficates()
{
$response = $this->request('v3/certificates', 'GET', [], false);
if (isset($response['code'])) {
throw new PayException($response['message']);
}
$certificates = $response['data'][0];
$certificates['certificates'] = $this->decrypt($certificates['encrypt_certificate']);
unset($certificates['encrypt_certificate']);
return $certificates;
}
}

View File

@@ -0,0 +1,526 @@
<?php
/**
* +----------------------------------------------------------------------
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
* +----------------------------------------------------------------------
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
* +----------------------------------------------------------------------
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
* +----------------------------------------------------------------------
* | Author: CRMEB Team <admin@crmeb.com>
* +----------------------------------------------------------------------
*/
namespace crmeb\services\wechat\v3pay;
use crmeb\exceptions\PayException;
use crmeb\services\wechat\config\V3PaymentConfig;
use crmeb\services\wechat\Payment;
use Psr\SimpleCache\InvalidArgumentException;
/**
* v3支付
* Class PayClient
* @package crmeb\services\easywechat\v3pay
*/
class PayClient extends BaseClient
{
//app支付
const API_APP_APY_URL = 'v3/pay/transactions/app';
//二维码支付截图
const API_NATIVE_URL = 'v3/pay/transactions/native';
//h5支付接口
const API_H5_URL = 'v3/pay/transactions/h5';
//jsapi支付接口
const API_JSAPI_URL = 'v3/pay/transactions/jsapi';
//发起商家转账API
const API_BATCHES_URL = 'v3/transfer/batches';
//退款
const API_REFUND_URL = 'v3/refund/domestic/refunds';
//退款查询接口
const API_REFUND_QUERY_URL = 'v3/refund/domestic/refunds/{out_refund_no}';
//发起转账
const API_TRANSFER_BILLS_URL = 'v3/fund-app/mch-transfer/transfer-bills';
//撤销转账
const API_TRANSFER_BILLS_CANCEL_URL = '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel';
//查询转账
const API_TRANSFER_QUERY_URL = 'v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}';
/**
* @var string
*/
protected string $type = Payment::WEB;
protected string $notify_url = '';
/**
* @param string $type
* @return $this
* @author 等风来
* @email 136327134@qq.com
* @date 2022/11/18
*/
public function setType(string $type)
{
$this->type = $type;
return $this;
}
/**
* 公众号jsapi支付下单
* @param string $openid
* @param string $outTradeNo
* @param string $total
* @param string $description
* @param string $attach
* @return mixed
* @throws InvalidArgumentException
*/
public function jsapiPay(string $openid, string $outTradeNo, string $total, string $description, string $attach)
{
$appId = $this->config->wechatAppid;
$res = $this->pay('jsapi', $appId, $outTradeNo, $total, $description, $attach, ['openid' => $openid]);
return $this->configForJSSDKPayment($appId, $res['prepay_id']);
}
/**
* 小程序支付
* @param string $openid
* @param string $outTradeNo
* @param string $total
* @param string $description
* @param string $attach
* @return array|false|string
* @throws InvalidArgumentException
*/
public function miniprogPay(string $openid, string $outTradeNo, string $total, string $description, string $attach)
{
$appId = $this->config->miniprogAppid;
$res = $this->pay('jsapi', $appId, $outTradeNo, $total, $description, $attach, ['openid' => $openid]);
return $this->configForJSSDKPayment($appId, $res['prepay_id']);
}
/**
* APP支付下单
* @param string $outTradeNo
* @param string $total
* @param string $description
* @param string $attach
* @return mixed
* @throws InvalidArgumentException
*/
public function appPay(string $outTradeNo, string $total, string $description, string $attach)
{
$res = $this->pay('app', $this->config->appAppid, $outTradeNo, $total, $description, $attach);
return $this->configForAppPayment($res['prepay_id']);
}
/**
* native支付下单
* @param string $outTradeNo
* @param string $total
* @param string $description
* @param string $attach
* @return mixed
* @throws InvalidArgumentException
*/
public function nativePay(string $outTradeNo, string $total, string $description, string $attach)
{
return $this->pay('native', $this->config->wechatAppid, $outTradeNo, $total, $description, $attach);
}
/**
* h5支付下单
* @param string $outTradeNo
* @param string $total
* @param string $description
* @param string $attach
* @return mixed
* @throws InvalidArgumentException
*/
public function h5Pay(string $outTradeNo, string $total, string $description, string $attach)
{
$res = $this->pay('h5', $this->config->wechatAppid, $outTradeNo, $total, $description, $attach);
return ['mweb_url' => $res['h5_url']];
}
/**
* 下单
* @param string $type
* @param string $appid
* @param string $outTradeNo
* @param string $total
* @param string $description
* @param string $attach
* @param array $payer
* @return mixed
* @throws InvalidArgumentException
*/
public function pay(string $type, string $appid, string $outTradeNo, string $total, string $description, string $attach, array $payer = [])
{
$totalFee = (int)bcmul($total, '100');
$data = [
'appid' => $appid,
'mchid' => $this->config->mchId,
'out_trade_no' => $outTradeNo,
'attach' => $attach,
'description' => $description,
'notify_url' => $this->config->notifyUrl,
'amount' => [
'total' => $totalFee,
'currency' => 'CNY'
],
];
if ($payer) {
$data['payer'] = $payer;
}
$url = '';
switch ($type) {
case 'h5':
$url = self::API_H5_URL;
$data['scene_info'] = [
'payer_client_ip' => request()->ip(),
'h5_info' => [
'type' => 'Wap'
]
];
break;
case 'native':
$url = self::API_NATIVE_URL;
break;
case 'app':
$url = self::API_APP_APY_URL;
break;
case 'jsapi':
$url = self::API_JSAPI_URL;
break;
}
if (!$url) {
throw new PayException('缺少请求地址');
}
$res = $this->request($url, 'POST', ['json' => $data]);
if (!$res) {
throw new PayException('微信支付:下单失败');
}
if (isset($res['code']) && isset($res['message'])) {
throw new PayException($res['message']);
}
return $res;
}
/**
* 发起转账新接口(2025年1月15日升级)
* @param string $outBatchNo
* @param string $amount
* @param string $userName
* @param string $remark
* @param array $transferDetailList
* @return void
* User: liusl
* DateTime: 2025/2/14 上午11:20
*/
/**
* @param string $outBatchNo
* @param string $amount
* @param $openid
* @param string $userName
* @param string $remark
* @param string $perception
* @param array $transferDetailList
* @return mixed
* @throws InvalidArgumentException
* User: liusl
* DateTime: 2025/2/14 下午2:14
*/
public function transferBills(string $outBatchNo, string $amount, $openid, string $userName, string $remark, string $perception, array $transferDetailList, string $transfer_scene_id = '1000')
{
$data = [
'transfer_amount' => (int)bcmul($amount, 100, 0),
'out_bill_no' => $outBatchNo,
'transfer_scene_id' => $transfer_scene_id,
'openid' => $openid,
'transfer_remark' => $remark,
'notify_url' => sys_config('site_url') . '/api/pay/mchNotify/'.$this->type,
'user_recv_perception' => '现金奖励',
'transfer_scene_report_infos' => $transferDetailList,
];
if ($amount >= 2000) {
if (empty($userName)) {
throw new PayException('明细金额大于等于2000时,收款人姓名必须填写');
}
$data['user_name'] = $this->encryptor($userName);
}
$appidMap = [
Payment::WEB => $this->config->wechatAppid,
Payment::MINI => $this->config->miniprogAppid,
Payment::APP => $this->config->appAppid,
];
if (!isset($appidMap[$this->type])) {
throw new PayException('暂时只支持微信用户、小程序用户、APP微信登录用户提现');
}
$data['appid'] = $appidMap[$this->type];
$res = $this->request(self::API_TRANSFER_BILLS_URL, 'POST', ['json' => $data]);
if (!$res || isset($res['code'], $res['message'])) {
throw new PayException($res['message'] ?? '微信支付:发起商家转账失败');
}
return $res;
}
/**
* 发起商家转账API
* @param string $outBatchNo
* @param string $amount
* @param string $batchName
* @param string $remark
* @param array $transferDetailList
* @return mixed
* @throws InvalidArgumentException
*/
public function batches(string $outBatchNo, string $amount, string $batchName, string $remark, array $transferDetailList)
{
$totalFee = '0';
$amount = bcadd($amount, '0', 2);
foreach ($transferDetailList as &$item) {
if ($item['transfer_amount'] >= 2000 && !empty($item['user_name'])) {
throw new PayException('明细金额大于等于2000时,收款人姓名必须填写');
}
$totalFee = bcadd($totalFee, $item['transfer_amount'], 2);
$item['transfer_amount'] = (int)bcmul($item['transfer_amount'], 100, 0);
if (isset($item['user_name'])) {
$item['user_name'] = $this->encryptor($item['user_name']);
}
}
if ($totalFee !== $amount) {
throw new PayException('转账明细金额总和和转账总金额不一致');
}
$amount = (int)bcmul($amount, 100, 0);
$appid = null;
if ($this->type === Payment::WEB) {
$appid = $this->config->wechatAppid;
} else if ($this->type === Payment::MINI) {
$appid = $this->config->miniprogAppid;
} else if ($this->type === Payment::APP) {
$appid = $this->config->appAppid;
}
if (!$appid) {
throw new PayException('暂时只支持微信用户、小程序用户、APP微信登录用户提现');
}
$data = [
'appid' => $appid,
'out_batch_no' => $outBatchNo,
'batch_name' => $batchName,
'batch_remark' => $remark,
'total_amount' => $amount,
'total_num' => count($transferDetailList),
'transfer_detail_list' => $transferDetailList
];
$res = $this->request(self::API_BATCHES_URL, 'POST', ['json' => $data]);
if (!$res) {
throw new PayException('微信支付:发起商家转账失败');
}
if (isset($res['code']) && isset($res['message'])) {
throw new PayException($res['message']);
}
return $res;
}
/**
* 退款
* @param string $outTradeNo
* @param array $options
* @return mixed
* @throws InvalidArgumentException
*/
public function refund(string $outTradeNo, array $options = [])
{
if (!isset($options['pay_price'])) {
throw new PayException(400730);
}
$totalFee = floatval(bcmul($options['pay_price'], 100, 0));
$refundFee = isset($options['refund_price']) ? floatval(bcmul($options['refund_price'], 100, 0)) : null;
$refundReason = $options['desc'] ?? '';
$refundNo = $options['refund_id'] ?? $outTradeNo;
/*仅针对老资金流商户使用
REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款)
REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款
*/
$refundAccount = $opt['refund_account'] ?? 'AVAILABLE';
$data = [
'transaction_id' => $outTradeNo,
'out_refund_no' => $refundNo,
'amount' => [
'refund' => (int)$refundFee,
'currency' => 'CNY',
'total' => (int)$totalFee
],
'funds_account' => $refundAccount
];
if ($refundReason) {
$data['reason'] = $refundReason;
}
$res = $this->request(self::API_REFUND_URL, 'POST', ['json' => $data]);
if (!$res) {
throw new PayException('微信支付:发起退款失败');
}
if (isset($res['code']) && isset($res['message'])) {
throw new PayException($res['message']);
}
return $res;
}
/**
* 查询退款
* @param string $outRefundNo
* @return mixed
* @throws InvalidArgumentException
*/
public function queryRefund(string $outRefundNo)
{
$res = $this->request($this->getApiUrl(self::API_REFUND_QUERY_URL, ['out_refund_no'], [$outRefundNo]), 'GET');
if (!$res) {
throw new PayException(500000);
}
if (isset($res['code']) && isset($res['message'])) {
throw new PayException($res['message']);
}
return $res;
}
/**
* jsapi支付
* @param string $appid
* @param string $prepayId
* @param bool $json
* @return array|false|string
*/
public function configForPayment(string $appid, string $prepayId, bool $json = true)
{
$params = [
'appId' => $appid,
'timeStamp' => strval(time()),
'nonceStr' => uniqid(),
'package' => "prepay_id=$prepayId",
'signType' => 'RSA',
];
$message = $params['appId'] . "\n" .
$params['timeStamp'] . "\n" .
$params['nonceStr'] . "\n" .
$params['package'] . "\n";
openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
$params['paySign'] = $sign;
return $json ? json_encode($params) : $params;
}
/**
* Generate app payment parameters.
* @param string $prepayId
* @return array
*/
public function configForAppPayment(string $prepayId): array
{
$params = [
'appid' => $this->config->wechatAppid,
'partnerid' => $this->config->mchId,
'prepayid' => $prepayId,
'noncestr' => uniqid(),
'timestamp' => time(),
'package' => 'Sign=WXPay',
];
$message = $params['appid'] . "\n" .
$params['timestamp'] . "\n" .
$params['noncestr'] . "\n" .
$params['prepayid'] . "\n";
openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
$params['sign'] = $sign;
return $params;
}
/**
* 小程序支付
* @param string $appid
* @param string $prepayId
* @return array|false|string
*/
public function configForJSSDKPayment(string $appid, string $prepayId)
{
$config = $this->configForPayment($appid, $prepayId, false);
$config['timestamp'] = $config['timeStamp'];
unset($config['timeStamp']);
return $config;
}
/**
* @param $callback
* @return \think\Response
*/
public function handleNotify($callback)
{
$request = request();
$event_type = $request->post('event_type');
// 判断成功事件
$successEvents = ['TRANSACTION.SUCCESS', 'MCHTRANSFER.BILL.FINISHED'];
$success = in_array($event_type, $successEvents);
$data = $this->decrypt($request->post('resource', []));
$handleResult = call_user_func_array($callback, [json_decode($data, true), $success]);
$response = [
'code' => $handleResult === true ? 'SUCCESS' : 'FAIL',
'message' => $handleResult === true ? 'OK' : $handleResult,
];
return response($response, 200, [], 'json');
}
public function queryTransferBills(string $outBillNo)
{
$res = $this->request($this->getApiUrl(self::API_TRANSFER_QUERY_URL, ['out_bill_no'], [$outBillNo]), 'GET');
if (!$res) {
throw new PayException(500000);
}
return $res;
}
}