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,167 @@
<?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;
use crmeb\exceptions\ApiException;
use think\facade\Log;
/**
* Class AccessTokenServeService
* @package crmeb\services
*/
class AccessTokenServeService extends HttpService
{
/**
* 配置
* @var string
*/
protected string $account;
/**
* @var string
*/
protected string $secret;
/**
* @var Cache|null
*/
protected $cache;
/**
* @var string
*/
protected string $cacheTokenPrefix = "_crmeb_plat";
/**
* @var string
*/
protected string $apiHost = 'http://sms.crmeb.net/api/';
/**
* 登录接口
*/
const USER_LOGIN = "v2/user/login";
/**
* AccessTokenServeService constructor.
* @param string $account
* @param string $secret
* @param null $cache
*/
public function __construct(string $account, string $secret, $cache = null)
{
if (!$cache) {
/** @var CacheService $cache */
$cache = app()->make(CacheService::class);
}
$this->account = $account;
$this->secret = $secret;
$this->cache = $cache;
}
/**
* 获取配置
* @return array
*/
public function getConfig(): array
{
return [
'access_key' => $this->account,
'secret_key' => $this->secret
];
}
/**
* 获取缓存token
* @return mixed
*/
public function getToken()
{
$accessTokenKey = md5($this->account . '_' . $this->secret . $this->cacheTokenPrefix);
$cacheToken = $this->cache->get($accessTokenKey);
if (!$cacheToken) {
$getToken = $this->getTokenFromServer();
$this->cache->set($accessTokenKey, $getToken['access_token'], 300);
$cacheToken = $getToken['access_token'];
}
return $cacheToken;
}
/**
* 从服务器获取token
* @return mixed
*/
public function getTokenFromServer()
{
$params = [
'access_key' => $this->account,
'secret_key' => $this->secret,
];
if (!$this->account || !$this->secret) {
throw new ApiException('请先登录一号通平台!');
}
$response = $this->postRequest($this->get(self::USER_LOGIN), $params);
$response = json_decode($response, true);
if (!$response) {
throw new ApiException('获取token失败');
}
if ($response['status'] === 200) {
return $response['data'];
} else {
throw new ApiException($response['msg']);
}
}
/**
* 请求
* @param string $url
* @param array $data
* @param string $method
* @param bool $isHeader
* @param array $header
* @return array|mixed
*/
public function httpRequest(string $url, array $data = [], string $method = 'POST', bool $isHeader = true, array $header = [])
{
if ($isHeader) {
$accessToken = $this->getToken();
if (!$accessToken) {
throw new ApiException('配置已更改或token已失效');
}
$header = array_merge($header, ['Authorization:Bearer-' . $accessToken]);
}
Log::info('请求接口:' . $url . ' 请求参数:' . json_encode($data, JSON_UNESCAPED_UNICODE));
$res = $this->request($this->get($url), $method, $data, $header);
if (!$res) {
throw new ApiException('平台错误:发生异常,请稍后重试');
}
$result = json_decode($res, true) ?: false;
if (!isset($result['status']) || $result['status'] != 200) {
throw new ApiException(isset($result['msg']) ? '平台错误:' . $result['msg'] : '平台错误:发生异常,请稍后重试');
}
Log::info('接口返回:' . json_encode($result, JSON_UNESCAPED_UNICODE));
return $result['data'] ?? [];
}
/**
* @param string $apiUrl
* @return string
*/
public function get(string $apiUrl = '')
{
return $this->apiHost . $apiUrl;
}
}

View File

@@ -0,0 +1,374 @@
<?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;
use Alipay\EasySDK\Payment\Common\Models\AlipayTradeFastpayRefundQueryResponse;
use Alipay\EasySDK\Payment\Common\Models\AlipayTradeRefundResponse;
use Alipay\EasySDK\Payment\Wap\Models\AlipayTradeWapPayResponse;
use crmeb\utils\Hook;
use JetBrains\PhpStorm\ArrayShape;
use think\facade\Log;
use think\facade\Route as Url;
use Alipay\EasySDK\Kernel\Config;
use Alipay\EasySDK\Kernel\Factory;
use crmeb\exceptions\PayException;
use app\services\pay\PayNotifyServices;
use Alipay\EasySDK\Kernel\Util\ResponseChecker;
use AlibabaCloud\Tea\Tea;
/**
* Class AliPayService
* @package crmeb\services
*/
class AliPayService
{
/**
* 配置
* @var array
*/
protected array $config = [
'appId' => '',
'merchantPrivateKey' => '',//应用私钥
'alipayPublicKey' => '',//支付宝公钥
'notifyUrl' => '',//可设置异步通知接收服务地址
'encryptKey' => '',//可设置AES密钥,调用AES加解密相关接口时需要(可选)
];
/**
* @var ResponseChecker
*/
protected ResponseChecker $response;
/**
* 付款码
* @var string
*/
protected string $authCode;
/**
* @var static
*/
protected static $instance;
/**
* AliPayService constructor.
* @param array $config
*/
protected function __construct(array $config = [])
{
if (!$config) {
$alipay = SystemConfigService::more(['ali_pay_appid', 'alipay_merchant_private_key', 'alipay_public_key', 'site_url']);
$config = [
'appId' => $alipay['ali_pay_appid'] ?? '',
'merchantPrivateKey' => $alipay['alipay_merchant_private_key'] ?? '',
'alipayPublicKey' => $alipay['alipay_public_key'] ?? '',
'notifyUrl' => ($alipay['site_url'] ?? '') . Url::buildUrl('/api/pay/notify/alipay'),
];
}
$this->config = array_merge($this->config, $config);
Factory::setOptions($this->getOptions());
$this->response = new ResponseChecker();
}
/**
* 实例化
* @param array $config
* @return static
*/
public static function instance(array $config = []): self
{
if (is_null(self::$instance)) {
self::$instance = new static($config);
}
return self::$instance;
}
/**
* 是否是支付宝付款码
* @param string $authCode
* @return bool
*/
public static function isAliPayAuthCode(string $authCode): bool
{
return preg_match('/^[0-9]{16,24}$/', $authCode) && in_array(substr($authCode, 0, 2), ['25', '26', '27', '28', '29', '30']);
}
/**
* 初始化
*/
protected function initialize()
{
Factory::setOptions($this->getOptions());
}
/**
* 设置配置
* @return Config
*/
protected function getOptions(): Config
{
$options = new Config();
$options->protocol = 'https';
$options->gatewayHost = 'openapi.alipay.com';
$options->signType = 'RSA2';
$options->appId = $this->config['appId'];
// 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中
$options->merchantPrivateKey = $this->config['merchantPrivateKey'];
//注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
$options->alipayPublicKey = $this->config['alipayPublicKey'];
//可设置异步通知接收服务地址(可选)
$options->notifyUrl = $this->config['notifyUrl'];
//可设置AES密钥,调用AES加解密相关接口时需要(可选)
if ($this->config['encryptKey']) {
$options->encryptKey = $this->config['encryptKey'];
}
Tea::config(['verify' => false]);
return $options;
}
/**
* 付款码付款
* @param string $authCode
* @param string $title
* @param string $orderId
* @param string $totalAmount
* @param string $passbackParams
* @return array
*/
#[ArrayShape([
'paid' => "int",
'message' => "mixed|string",
'payInfo' => "array"
])]
public function microPay(string $authCode, string $title, string $orderId, string $totalAmount, string $passbackParams): array
{
$title = trim($title);
try {
$result = Factory::payment()->faceToFace()->optional('passback_params', $passbackParams)->pay($title, $orderId, $totalAmount, $authCode);
if ($this->response->success($result)) {
$response = $result->toMap();
return [
'paid' => $response['code'] === '10000' ? 1 : 0,
'message' => $response['sub_msg'] ?? '支付成功',
'payInfo' => $response
];
} else {
throw new PayException('失败原因:' . $result->msg . ',' . $result->subMsg);
}
} catch (\Exception $e) {
throw new PayException($e->getMessage());
}
}
/**
* 创建订单
* @param string $title 商品名称
* @param string $orderId 订单号
* @param string $totalAmount 支付金额
* @param string $passbackParams 备注
* @param string $quitUrl 同步跳转地址
* @param string $siteUrl
* @param bool $isCode
*/
public function create(string $title, string $orderId, string $totalAmount, string $passbackParams, string $quitUrl = '', string $siteUrl = '', bool $isCode = false)
{
$title = trim($title);
try {
if ($isCode) {
//二维码支付
$result = Factory::payment()->faceToFace()->optional('passback_params', $passbackParams)->precreate($title, $orderId, $totalAmount);
} else if (request()->isApp()) {
//app支付
$result = Factory::payment()->app()->optional('passback_params', $passbackParams)->pay($title, $orderId, $totalAmount);
} else {
//h5支付
$result = Factory::payment()->wap()->optional('passback_params', $passbackParams)->pay($title, $orderId, $totalAmount, $quitUrl, $siteUrl);
}
if ($this->response->success($result)) {
return $result->body ?? $result;
} else {
throw new PayException('失败原因:' . $result->msg . ',' . $result->subMsg);
}
} catch (\Exception $e) {
throw new PayException($e->getMessage());
}
}
/**
* 订单退款
* @param string $outTradeNo 订单号
* @param string $totalAmount 退款金额
* @param string $refund_id 退款单号
* @return AlipayTradeRefundResponse
*/
public function refund(string $outTradeNo, string $totalAmount, string $refund_id): AlipayTradeRefundResponse
{
try {
$result = Factory::payment()->common()->refund($outTradeNo, $totalAmount, $refund_id);
if ($this->response->success($result)) {
return $result;
} else {
throw new PayException('失败原因:' . $result->msg . ',' . $result->subMsg);
}
} catch (\Exception $e) {
throw new PayException($e->getMessage());
}
}
/**
* 查询交易退款单号信息
* @param string $outTradeNo
* @param string $outRequestNo
* @return AlipayTradeFastpayRefundQueryResponse
*/
public function queryRefund(string $outTradeNo, string $outRequestNo): AlipayTradeFastpayRefundQueryResponse
{
try {
$result = Factory::payment()->common()->queryRefund($outTradeNo, $outRequestNo);
if ($this->response->success($result)) {
return $result;
} else {
throw new PayException('失败原因:' . $result->msg . ',' . $result->subMsg);
}
} catch (\Exception $e) {
throw new PayException($e->getMessage());
}
}
/**
* 支付异步回调
* @return string
*/
public static function handleNotify()
{
return self::instance()->notify(function ($notify) {
if (isset($notify->out_trade_no)) {
if (isset($notify->attach) && $notify->attach) {
if (($count = strpos($notify->out_trade_no, '_')) !== false) {
$notify->trade_no = $notify->out_trade_no;
$notify->out_trade_no = substr($notify->out_trade_no, $count + 1);
}
return (new Hook(PayNotifyServices::class, 'aliyun'))->listen($notify->attach, $notify->out_trade_no, $notify->trade_no);
}
return false;
}
});
}
/**
* 异步回调
* @param callable $notifyFn
* @return string
*/
public function notify(callable $notifyFn)
{
// app()->request->filter(['trim']);
// $paramInfo = app()->request->postMore([
// ['gmt_create', ''],
// ['charset', ''],
// ['seller_email', ''],
// ['subject', ''],
// ['sign', ''],
//// ['buyer_id', ''],
// ['invoice_amount', ''],
// ['notify_id', ''],
// ['fund_bill_list', ''],
// ['notify_type', ''],
// ['trade_status', ''],
// ['receipt_amount', ''],
// ['buyer_pay_amount', ''],
// ['app_id', ''],
// ['seller_id', ''],
// ['sign_type', ''],
// ['gmt_payment', ''],
// ['notify_time', ''],
// ['passback_params', ''],
// ['version', ''],
// ['out_trade_no', ''],
// ['total_amount', ''],
// ['trade_no', ''],
// ['auth_app_id', ''],
// ['buyer_logon_id', ''],
// ['point_amount', ''],
// ], false, false);
$paramInfo = app()->request->param();
unset($paramInfo['type']);
//卡劵红包类
// $voucher_detail_list = app()->request->param('voucher_detail_list');
// if ($voucher_detail_list) {
// $paramInfo['voucher_detail_list'] = $voucher_detail_list;
// }
//商户订单号
$postOrder['out_trade_no'] = $paramInfo['out_trade_no'] ?? '';
//支付宝交易号
$postOrder['trade_no'] = $paramInfo['trade_no'] ?? '';
//交易状态
$postOrder['trade_status'] = $paramInfo['trade_status'] ?? '';
//备注
$postOrder['attach'] = isset($paramInfo['passback_params']) ? urldecode($paramInfo['passback_params']) : '';
if (in_array($postOrder['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED']) && $this->verifyNotify($paramInfo)) {
try {
if ($notifyFn((object)$postOrder)) {
return 'success';
}
} catch (\Exception $e) {
Log::error('支付宝异步会回调成功,执行函数错误。错误单号:' . $postOrder['out_trade_no']);
}
}
return 'fail';
}
/**
* 验签
* @param array $param
* @return bool
*/
protected function verifyNotify(array $param): bool
{
try {
return Factory::payment()->common()->verifyNotify($param);
} catch (\Exception $e) {
Log::error('支付宝回调成功,验签发生错误,错误原因:' . $e->getMessage());
}
return false;
}
/**
* 商家支付接口
* @param array $bizParams
* @return \Alipay\EasySDK\Util\Generic\Models\AlipayOpenApiGenericResponse|false
* User: liusl
* DateTime: 2024/10/21 12:01
*/
public function merchantPay(array $bizParams)
{
try {
// 调用工厂类的通用方法执行支付宝转账操作
$result = Factory::util()->generic()->execute('alipay.fund.trans.toaccount.transfer', [], $bizParams);
// 判断支付是否成功
if ($this->response->success($result)) {
return $result;
} else {
return false;
}
} catch (\Exception $e) {
// 记录日志并返回false
Log::error('支付宝转账失败,失败原因:' . $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,436 @@
<?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;
use think\facade\Cache as CacheStatic;
/**
* crmeb 缓存类
* Class CacheService
* @package crmeb\services
* @mixin \Redis
*/
class CacheService
{
const CACHE_EXPIRE_NAME = 'cache_expire';
//副屏相关
const CASHIER_AUX_SCREEN_TAG = 'auxScreen';
/**
* 标签名
* @var string
*/
protected static $globalCacheName = '_cached_1515146130';
/**
* 缓存队列key
* @var string[]
*/
protected static $redisQueueKey = [
0 => 'product',
1 => 'seckill',
2 => 'bargain',
3 => 'combination',
4 => 'integral',
5 => 'discounts',
6 => 'lottery'
];
/**
* 过期时间
* @var int
*/
protected static $expire;
/**
* 获取缓存过期时间
* @param int|null $expire
* @return int
*/
protected static function getExpire(int $expire = null): int
{
if (self::$expire) {
return (int)self::$expire;
}
$expire = !is_null($expire) ? $expire : CacheStatic::store('redis')->get(self::CACHE_EXPIRE_NAME, 360);
if (!is_int($expire))
$expire = (int)$expire;
return self::$expire = $expire;
}
/**
* 判断缓存是否存在
* @param string $name
* @return bool
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public static function has(string $name): bool
{
try {
return CacheStatic::has($name);
} catch (\Throwable $e) {
return false;
}
}
/**
* 写入缓存
* @param string $name 缓存名称
* @param mixed $value 缓存值
* @param int $expire 缓存时间,为0读取系统缓存时间
* @return bool
*/
public static function set(string $name, $value, int $expire = null): bool
{
try {
return self::handler()->set($name, $value, $expire ?? self::getExpire($expire));
} catch (\Throwable $e) {
return false;
}
}
/**
* 如果不存在则写入缓存
* @param string $name
* @param bool $default
* @return mixed
*/
public static function get(string $name, $default = false, int $expire = null)
{
try {
return self::handler()->remember($name, $default, $expire ?? self::getExpire($expire));
} catch (\Throwable $e) {
try {
if (is_callable($default)) {
return $default();
} else {
return $default;
}
} catch (\Throwable $e) {
return null;
}
}
}
/**
* 如果不存在则写入缓存
* @param string $name
* @param bool $default
* @return mixed
*/
public static function remember(string $name, $default = false, int $expire = null)
{
try {
return self::handler()->remember($name, $default, $expire ?? self::getExpire($expire));
} catch (\Throwable $e) {
try {
if (is_callable($default)) {
return $default();
} else {
return $default;
}
} catch (\Throwable $e) {
return null;
}
}
}
/**
* 删除缓存
* @param string $name
* @return bool
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public static function delete(string $name)
{
return CacheStatic::delete($name);
}
/**
* 缓存句柄
*
* @return \think\cache\TagSet|CacheStatic
*/
public static function handler(?string $cacheName = null)
{
return CacheStatic::tag($cacheName ?: self::$globalCacheName);
}
/**
* 清空缓存池
* @return bool
*/
public static function clear()
{
return self::handler()->clear();
}
/**
* Redis缓存句柄
*
* @return \think\cache\TagSet|CacheStatic
*/
public static function redisHandler($type = null)
{
if ($type) {
return CacheStatic::store('redis')->tag($type);
} else {
return CacheStatic::store('redis');
}
}
/**
* 放入令牌桶
* @param string $key
* @param array $value
* @param string $type
* @return bool
*/
public static function setTokenBucket(string $key, $value, $expire = null, string $type = 'admin')
{
try {
$redisCahce = self::redisHandler($type);
return $redisCahce->set($key, $value, $expire);
} catch (\Throwable $e) {
return false;
}
}
/**
* 清除所有令牌桶
* @param string $type
* @return bool
*/
public static function clearTokenAll(string $type = 'admin')
{
try {
return self::redisHandler($type)->clear();
} catch (\Throwable $e) {
return false;
}
}
/**
* 清除令牌桶
* @param string $type
* @return bool
*/
public static function clearToken(string $key)
{
try {
return self::redisHandler()->delete($key);
} catch (\Throwable $e) {
return false;
}
}
/**
* 查看令牌是否存在
* @param string $key
* @return bool
*/
public static function hasToken(string $key)
{
try {
return self::redisHandler()->has($key);
} catch (\Throwable $e) {
return false;
}
}
/**
* 获取token令牌桶
* @param string $key
* @return mixed|null
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public static function getTokenBucket(string $key)
{
try {
return self::redisHandler()->get($key, null);
} catch (\Throwable $e) {
return null;
}
}
/**
* 获取指定分数区间的成员
* @param $key
* @param int $start
* @param int $end
* @param array $options
* @return mixed
*/
public static function zRangeByScore($key, $start = '-inf', $end = '+inf', array $options = [])
{
return self::redisHandler()->zRangeByScore($key, $start, $end, $options);
}
/**
* 设置redis入库队列
* @param string $unique
* @param int $number
* @param int $type
* @param bool $isPush true :重置 false:累加
* @return bool
*/
public static function setStock(string $unique, int $number, int $type = 1, bool $isPush = true)
{
if (!$unique || !$number) return false;
$name = (self::$redisQueueKey[$type] ?? '') . '_' . $type . '_' . $unique;
/** @var self $cache */
$cache = self::redisHandler();
$res = true;
if ($isPush) {
$cache->del($name);
}
$data = [];
for ($i = 1; $i <= $number; $i++) {
$data[] = $i;
}
$res = $res && $cache->lPush($name, ...$data);
return $res;
}
/**
* 弹出redis队列中的库存条数
* @param string $unique
* @param int $number
* @param int $type
* @return bool
*/
public static function popStock(string $unique, int $number, int $type = 1)
{
if (!$unique || !$number) return false;
$name = (self::$redisQueueKey[$type] ?? '') . '_' . $type . '_' . $unique;
/** @var self $cache */
$cache = self::redisHandler();
$res = true;
if ($number > $cache->lLen($name)) {
return false;
}
for ($i = 1; $i <= $number; $i++) {
$res = $res && $cache->lPop($name);
}
return $res;
}
/**
* 是否有库存|返回库存
* @param string $unique
* @param int $number
* @param int $type
* @return bool
*/
public static function checkStock(string $unique, int $number = 0, int $type = 1)
{
$name = (self::$redisQueueKey[$type] ?? '') . '_' . $type . '_' . $unique;
if ($number) {
return self::redisHandler()->lLen($name) >= $number;
} else {
return self::redisHandler()->lLen($name);
}
}
/**
* 检查锁
* @param int $uid
* @param int $timeout
* @return bool
* @author 等风来
* @email 136327134@qq.com
* @date 2022/11/22
*/
public static function setMutex(string $key, int $timeout = 10)
{
$curTime = time();
$readMutexKey = "redis:mutex:{$key}";
$mutexRes = self::redisHandler()->handler()->setnx($readMutexKey, $curTime + $timeout);
if ($mutexRes) {
return true;
}
//就算意外退出,下次进来也会检查key,防止死锁
$time = self::redisHandler()->handler()->get($readMutexKey);
if ($curTime > $time) {
self::redisHandler()->handler()->del($readMutexKey);
return self::redisHandler()->handler()->setnx($readMutexKey, $curTime + $timeout);
}
return false;
}
/**
* 删除锁
* @param $uid
* @author 等风来
* @email 136327134@qq.com
* @date 2022/11/22
*/
public static function delMutex(string $key)
{
$readMutexKey = "redis:mutex:{$key}";
self::redisHandler()->handler()->del($readMutexKey);
}
/**
* 数据库锁
* @param $key
* @param $fn
* @param int $ex
* @return bool|mixed
*/
public static function lock($key, $fn = [], int $ex = 10)
{
$service = app()->make(LockService::class);
if ($fn instanceof \Closure) {
return $service->exec($key, $fn, $ex);
} else {
return $service->lock($key, 1, $ex);
}
}
/**
* 销毁锁
* @param $key
* @return bool
*/
public static function unLock($key)
{
return app()->make(LockService::class)->unlock($key);
}
/**
* 魔术方法
* @param $name
* @param $arguments
* @return mixed
*/
public static function __callStatic($name, $arguments)
{
return self::redisHandler()->{$name}(...$arguments);
}
/**
* 魔术方法
* @param $name
* @param $arguments
* @return mixed
*/
public function __call($name, $arguments)
{
return self::redisHandler()->{$name}(...$arguments);
}
}

View File

@@ -0,0 +1,596 @@
<?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;
/**
* 复制主流商城商品
* Class CopyProductService
* @package crmeb\services
*/
class CopyProductService
{
//接口地址
protected static $api = [
'taobao' => 'https://api03.6bqb.com/taobao/detail', //https://api03.6bqb.com/app/taobao/detail
'tmall' => 'https://api03.6bqb.com/tmall/detail',
'jd' => 'https://api03.6bqb.com/jd/detail',
'pdd' => 'https://api03.6bqb.com/pdd/detail',
'suning' => 'https://api03.6bqb.com/suning/detail',
'1688' => 'https://api03.6bqb.com/alibaba/detail'
];
protected static $apiKey = '';//996EF05B079F8706345938A0CD7339BB
//商品默认字段
protected static $productInfo = [
'cate_id' => '',
'store_name' => '',
'store_info' => '',
'unit_name' => '件',
'price' => 0,
'keyword' => '',
'ficti' => 0,
'ot_price' => 0,
'give_integral' => 0,
'postage' => 0,
'cost' => 0,
'image' => '',
'slider_image' => '',
'video_link' => '',
'add_time' => 0,
'stock' => 0,
'description' => '',
'description_images' => [],
'soure_link' => '',
'temp_id' => '',
'items' => [],
'attrs' => [],
'info' => [],
];
/**
* 整合
* @param $url
* @param $method
* @param $data
* @return string
*/
public static function makeUrl(string $url, string $method, array $data)
{
$param = '';
if (strtolower($method) == 'get' && $data) {
foreach ($data as $key => $value) {
$param .= '&' . $key . '=' . $value;
}
}
return $url . '?apikey=' . self::$apiKey . $param;
}
/**
* @param bool $status
* @param string $msg
* @param array $data
*/
public static function setReturn(bool $status = true, string $msg = 'SUCCESS', array $data = [])
{
return ['status' => $status, 'msg' => $msg, 'data' => $data];
}
/**
*
* @param array $data
*/
public static function getInfo(string $type = 'taobao', array $data = [], string $apikey = '')
{
if (!$apikey) {
return self::setReturn(false, '请先去设置复制商品apiKey');
}
$url = self::$api[$type] ?? '';
$type = $type == '1688' ? 'alibaba' : $type;
$action = $type . 'Info';
$deal_action = $type . 'Deal';
$method = 'get';
self::$apiKey = $apikey;
if (!$data || !$url || !is_callable(self::class, $action) || !is_callable(self::class, $deal_action)) {
return self::setReturn(false, '暂不支持该平台商品复制');
}
switch ($type) {
case 'taobao':
case 'tmall':
case 'jd':
case 'pdd':
case 'alibaba':
$method = 'get';
if (!isset($data['itemid']) || !$data['itemid'])
return self::setReturn(false, '缺少商品ID');
break;
case 'suning':
$method = 'get';
if (!isset($data['itemid']) || !$data['itemid'])
return self::setReturn(false, '缺少商品ID');
if (!isset($data['shopid']) || !$data['shopid'])
return self::setReturn(false, '缺少商户ID');
break;
}
$url = self::makeUrl($url, $method, $data);
if ($cache_info = CacheService::get(md5($url))) {
return self::setReturn(true, 'SUCCESS', $cache_info);
}
$info = self::$action($url, $data);
if (!$info) return self::setReturn(false, '获取商品失败');
$info = json_decode($info, true);
if (!$info || (!in_array($info['retcode'], ['0000']))) {
return self::setReturn(false, $info['message'] ?? '获取商品失败', $info);
}
$result = $info['data'];
//可能存在下一页 但是api中没有分页参数 暂留
// if (isset($info['hasNext']) && $info['hasNext']) {
// $data['page'] = $info['page'] + 1;
// }
$result = self::$deal_action($result);
//过滤采集到的规格 删除其中的空值
if ($result['items']) {
foreach ($result['items'] as $k => $item) {
if (isset($item['value'])) {
if ($item['value'] == '') unset($result['items'][$k]);
if (!$item['detail'] || !isset($item['detail'][0]) || $item['detail'][0] == '') unset($result['items'][$k]);
} else {
unset($result['items'][$k]);
}
}
}
if (!$result['items']) {
$result['items'] = [
[
'value' => '默认',
'detail' => [
'默认'
]
]
];
}
$result['info'] = self::formatAttr(array_values($result['items']));
if (!$result['image'] && $result['slider_image'])
$result['image'] = $result['slider_image'][0] ?? '';
if ($result['description']) {
$result['description'] = str_replace('data-lazyload', 'src', $result['description']);
}
CacheService::set(md5($url), $result, 3600 * 24);
return self::setReturn(true, 'SUCCESS', $result);
}
/**
* 获取淘宝商品
* @param $url
* @param $data
* @param string $method
* @return bool|string
*/
public static function taobaoInfo(string $url, array $data, string $method = 'get')
{
$info = HttpService::request($url, $method, $data);
$result = false;
if ($info) {
$result = $info;
}
return $result;
}
/**
* 处理获取淘宝的商品
* @param $data
* @return mixed
*/
public static function taobaoDeal(array $data)
{
$info = $data['item'] ?? [];
$result = self::$productInfo;
if ($info) {
$result['store_name'] = $info['title'] ?? '';
$result['store_info'] = $info['subTitle'] ?? '';
$result['slider_image'] = $info['images'] ?? '';
$result['description'] = $info['desc'] ?? '';
$result['description_images'] = $info['descImgs'] ?? [];
$items = [];
if (isset($info['props']) && $info['props']) {
foreach ($info['props'] as $key => $prop) {
$item['value'] = $prop['name'];
$item['detail'] = [];
foreach ($prop['values'] as $name) {
$item['detail'][] = $name['name'];
}
$items[] = $item;
}
}
$result['items'] = $items;
}
return $result;
}
/**
* 获取天猫商品
* @param $url
* @param $data
* @param string $method
* @return bool|string
*/
public static function tmallInfo(string $url, array $data, string $method = 'get')
{
$info = HttpService::request($url, $method, $data);
$result = false;
if ($info) {
$result = $info;
}
return $result;
}
/**
* 处理天猫商品
* @param $data
* @return mixed
*/
public static function tmallDeal(array $data)
{
$info = $data['item'] ?? [];
$result = self::$productInfo;
if ($info) {
$result['store_name'] = $info['title'] ?? '';
$result['store_info'] = $info['subTitle'] ?? '';
$result['slider_image'] = self::lingByHttp($info['images'] ?? '');
$result['description_images'] = self::lingByHttp($info['descImgs'] ?? []);
$description = '';
foreach ($result['description_images'] as $item) {
$description .= '<img src="' . $item . '">';
}
unset($item);
$result['description'] = $description;
$items = [];
if (isset($info['props']) && $info['props']) {
foreach ($info['props'] as $key => $prop) {
$item['value'] = $prop['name'];
$item['detail'] = [];
foreach ($prop['values'] as $name) {
$item['detail'][] = $name['name'];
}
$items[] = $item;
}
}
$result['items'] = $items;
}
return $result;
}
/**
* 获取京东商品
* @param $url
* @param $data
* @param string $method
* @return bool|string
*/
public static function jdInfo(string $url, array $data, string $method = 'get')
{
$info = HttpService::request($url, $method, $data);
$result = false;
if ($info) {
$result = $info;
}
return $result;
}
/**
* 处理京东商品
* @param $data
* @return mixed
*/
public static function jdDeal(array $data)
{
$info = $data['item'] ?? [];
$result = self::$productInfo;
if ($info) {
$result['store_name'] = $info['name'] ?? '';
$result['store_info'] = $result['store_name'];
$result['price'] = $info['price'] ?? 0;
$result['ot_price'] = $info['originalPrice'] ?? 0;
$result['slider_image'] = $info['images'] ?? [];
$result['description'] = $info['desc'] ?? '';
$result['description_images'] = $info['descImgs'] ?? [];
$result['description_images'] = array_map(function ($item) {
if (strstr($item, 'http') === false) {
$item = 'http:' . $item;
}
return $item;
}, $result['description_images']);
if (strstr($result['description'], '<style>') !== false && strstr($result['description'], '<img') === false) {
$content = '';
foreach ($result['description_images'] as $item) {
$content .= '<p><img src="' . $item . '"></p>';
}
$result['description'] = $content;
}
$items = [];
if (isset($info['skuProps']) && $info['skuProps']) {
foreach ($info['skuProps'] as $key => $prop) {
$item = [];
$item['value'] = $info['saleProp'][$key] ?? '';
$item['detail'] = $prop;
$items[] = $item;
}
}
$result['items'] = $items;
}
return $result;
}
/**
* @param $data
* @return array|string
*/
public static function lingByHttp($data)
{
if (is_array($data)) {
foreach ($data as &$item) {
if (strstr($item, 'http://') === false && strstr($item, 'https://') === false) {
$item = 'http:' . $item;
}
}
return $data;
} else {
if (strstr($data, 'http://') === false && strstr($data, 'https://') === false) {
$data = 'http:' . $data;
}
return $data;
}
}
/**
* 获取拼多多商品
* @param $url
* @param $data
* @param string $method
* @return bool|string
*/
public static function pddInfo(string $url, array $data, string $method = 'get')
{
$info = HttpService::request($url, $method, $data);
$result = false;
if ($info) {
$result = $info;
}
return $result;
}
/**
* 处理拼多多商品
* @param $data
* @return mixed
*/
public static function pddDeal(array $data)
{
$info = $data['item'] ?? [];
$result = self::$productInfo;
if ($info) {
$result['store_name'] = $info['goodsName'] ?? '';
$result['store_info'] = $info['goodsDesc'] ?? '';
$result['image'] = $info['thumbUrl'] ?? '';
$result['slider_image'] = $info['banner'] ?? [];
$image = [];
foreach ($result['slider_image'] as &$item) {
if (is_array($item) && isset($item['url'])) {
$image[] = $item['url'];
}
}
if ($image) {
$result['slider_image'] = $image;
}
$result['video_link'] = $info['video']['videoUrl'] ?? '';
$result['price'] = $info['maxNormalPrice'] ?? 0;
$result['ot_price'] = $info['marketPrice'] ?? 0;
$descImgs = [];
if (isset($info['detail']) && $info['detail']) {
foreach ($info['detail'] as $img) {
if (isset($img['url']) && $img['url']) $descImgs[] = $img['url'];
}
}
$result['description_images'] = $descImgs;
$desc = '<p>';
foreach ($descImgs as $item) {
$desc .= '<img src="' . $item . '"/>';
}
$desc .= '</p>';
$result['description'] = $desc;
$items = [];
if (isset($info['skus']) && $info['skus']) {
$i = 0;
foreach ($info['skus'] as $sku) {
foreach ($sku['specs'] as $key => $spec) {
if ($i == 0) $items[$key]['value'] = $spec['spec_key'];
$items[$key]['detail'][] = $spec['spec_value'];
}
$i++;
}
}
foreach ($items as $k => $item) {
$items[$k]['detail'] = array_unique($item['detail']);
}
$result['items'] = $items;
}
return $result;
}
/**
* 获取苏宁商品
* @param $url
* @param $data
* @param string $method
* @return bool|string
*/
public static function suningInfo(string $url, array $data, string $method = 'get')
{
$info = HttpService::request($url, $method, $data);
$result = false;
if ($info) {
$result = $info;
}
return $result;
}
/**
*
* @param string $url
* @param array $data
* @param string $method
* @return bool|string
*/
public static function alibabaInfo(string $url, array $data, string $method = 'get')
{
$info = HttpService::request($url, $method, $data);
$result = false;
if ($info) {
$result = $info;
}
return $result;
}
/**
* @param array $data
* @return array
*/
public static function alibabaDeal(array $data)
{
$result = self::$productInfo;
if ($data) {
$result['store_name'] = $data['title'] ?? '';
$result['store_info'] = $result['store_name'];
$result['slider_image'] = $data['images'] ?? [];
$result['price'] = $data['price'] ?? 0;
$result['description'] = $data['desc'] ?? '';
$items = [];
if (isset($data['skuProps']) && $data['skuProps']) {
$i = 0;
foreach ($data['skuProps'] as $passSUb) {
$items[$i]['value'] = $passSUb['prop'];
$items[$i]['detail'] = array_column($passSUb['value'], 'name');
$i++;
}
}
foreach ($items as $k => $item) {
$items[$k]['detail'] = array_unique($item['detail']);
}
$result['items'] = $items;
}
return $result;
}
/**
* 处理苏宁商品
* @param $data
* @return mixed
*/
public static function suningDeal(array $data)
{
$result = self::$productInfo;
if ($data) {
$result['store_name'] = $data['title'] ?? '';
$result['store_info'] = $result['store_name'];
$result['slider_image'] = $data['images'] ?? [];
$result['price'] = $data['price'] ?? 0;
$result['description'] = $data['desc'] ?? '';
$items = [];
if (isset($data['passSubList']) && $data['passSubList']) {
$i = 0;
foreach ($data['passSubList'] as $passSUb) {
$j = 0;
foreach ($passSUb as $key => $sub) {
if ($i == 0) $items[$j]['value'] = $key;
foreach ($sub as $value) {
if (isset($value['characterValueDisplayName']) && $value['characterValueDisplayName'])
$items[$j]['detail'][] = $value['characterValueDisplayName'];
}
$j++;
}
$i++;
}
}
foreach ($items as $k => $item) {
$items[$k]['detail'] = array_unique($item['detail']);
}
$result['items'] = $items;
}
return $result;
}
/**
* 格式化规格
* @param $attr
* @return array
*/
public static function formatAttr(array $attr)
{
$value = attr_format($attr)[1];
$valueNew = [];
$count = 0;
foreach ($value as $key => $item) {
$detail = $item['detail'];
// sort($item['detail'], SORT_STRING);
$suk = implode(',', $item['detail']);
$sukValue[$suk]['pic'] = '';
$sukValue[$suk]['price'] = 0;
$sukValue[$suk]['cost'] = 0;
$sukValue[$suk]['ot_price'] = 0;
$sukValue[$suk]['stock'] = 0;
$sukValue[$suk]['bar_code'] = '';
$sukValue[$suk]['weight'] = 0;
$sukValue[$suk]['volume'] = 0;
$sukValue[$suk]['brokerage'] = 0;
$sukValue[$suk]['brokerage_two'] = 0;
foreach (array_keys($detail) as $k => $title) {
if ($title == '') continue;
$header[$k]['title'] = $title;
$header[$k]['align'] = 'center';
$header[$k]['minWidth'] = 120;
}
foreach (array_values($detail) as $k => $v) {
if ($v == '') continue;
$valueNew[$count]['value' . ($k + 1)] = $v;
$header[$k]['key'] = 'value' . ($k + 1);
}
$valueNew[$count]['detail'] = $detail;
$valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? '';
$valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0;
$valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0;
$valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0;
$valueNew[$count]['vip_price'] = isset($sukValue[$suk]['vip_price']) ? floatval($sukValue[$suk]['vip_price']) : 0;
$valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0;
$valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? '';
$valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0;
$valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0;
$valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0;
$valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0;
$count++;
}
$header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 80];
$header[] = ['title' => '售价', 'slot' => 'price', 'align' => 'center', 'minWidth' => 95];
$header[] = ['title' => '成本价', 'slot' => 'cost', 'align' => 'center', 'minWidth' => 95];
$header[] = ['title' => '划线价', 'slot' => 'ot_price', 'align' => 'center', 'minWidth' => 95];
$header[] = ['title' => '会员价', 'slot' => 'vip_price', 'align' => 'center', 'minWidth' => 140];
$header[] = ['title' => '库存', 'slot' => 'stock', 'align' => 'center', 'minWidth' => 95];
$header[] = ['title' => '商品编号', 'slot' => 'bar_code', 'align' => 'center', 'minWidth' => 120];
$header[] = ['title' => '重量(KG)', 'slot' => 'weight', 'align' => 'center', 'minWidth' => 95];
$header[] = ['title' => '体积(m³)', 'slot' => 'volume', 'align' => 'center', 'minWidth' => 95];
$header[] = ['title' => '操作', 'slot' => 'action', 'align' => 'center', 'minWidth' => 70];
$info = ['attr' => $attr, 'value' => $valueNew, 'header' => $header];
return $info;
}
}

View File

@@ -0,0 +1,62 @@
<?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;
use crmeb\interfaces\DeliveryInterface;
use crmeb\services\delivery\Delivery;
use crmeb\services\delivery\storage\Dada;
use crmeb\services\delivery\store\Uupt;
/**
* Class BaseExpress
* @package crmeb\basic
*/
class DeliverySevices
{
protected static $delivery = [];
const DELIVERY_TYPE_DADA = 1;
const DELIVERY_TYPE_UU = 2;
/**
* @param int $type
* @param bool $is_cache
* @return Delivery|mixed
*/
public static function init(int $type = self::DELIVERY_TYPE_DADA, bool $is_cache = false)
{
$type = (int)$type;
if ($is_cache && isset(self::$delivery['delivery_' . $type]) && self::$delivery['delivery_' . $type]) {
return self::$delivery['delivery_' . $type];
}
$config = [];
switch ($type) {
case 1:
$config = [
'app_key' => sys_config('dada_app_key'),
'app_secret' => sys_config('dada_app_sercret'),
'source_id' => sys_config('dada_source_id'),
];
break;
case 2:
$config = [
'app_key' => sys_config('uupt_appkey'),
'app_id' => sys_config('uupt_app_id'),
'open_id' => sys_config('uupt_open_id'),
];
break;
}
return self::$delivery['delivery_' . $type] = new Delivery($type, $config);
}
}

View File

@@ -0,0 +1,217 @@
<?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;
use crmeb\exceptions\AdminException;
use think\exception\ValidateException;
use think\Image;
class DownloadImageService
{
//是否生成缩略图
protected $thumb = false;
//缩略图宽度
protected $thumbWidth = 300;
//缩略图高度
protected $thumHeight = 300;
//存储位置
protected $path = 'attach';
protected $rules = ['thumb', 'thumbWidth', 'thumHeight', 'path'];
/**
* 获取即将要下载的图片扩展名
* @param string $url
* @param string $ex
* @return array|string[]
*/
public function getImageExtname($url = '', $ex = 'jpg')
{
$_empty = ['file_name' => '', 'ext_name' => $ex];
if (!$url) return $_empty;
if (strpos($url, '?')) {
$_tarr = explode('?', $url);
$url = trim($_tarr[0]);
}
$arr = explode('.', $url);
if (!is_array($arr) || count($arr) <= 1) return $_empty;
$ext_name = trim($arr[count($arr) - 1]);
$ext_name = !$ext_name ? $ex : $ext_name;
return ['file_name' => md5($url) . '.' . $ext_name, 'ext_name' => $ext_name];
}
/**
* 下载图片
* @param string $url
* @param string $name
* @param int $upload_type
* @return mixed
*/
public function downloadImage(string $url, $name = '')
{
if (!$name) {
// 获取要下载的文件名称
$downloadImageInfo = $this->getImageExtname($url);
$ext = $downloadImageInfo['ext_name'];
$name = $downloadImageInfo['file_name'];
if (!$name) throw new ValidateException('上传图片不存在');
} else {
$ext = $this->getImageExtname($name)['ext_name'];
}
if (!in_array($ext, ['png', 'jpg', 'jpeg', 'gif'])) {
throw new ValidateException('格式错误,文件后缀不允许');
}
if (strstr($url, 'http://') === false && strstr($url, 'https://') === false) {
$url = 'http:' . $url;
}
$url = str_replace('https://', 'http://', $url);
if ($this->path == 'attach') {
$date_dir = date('Y') . DIRECTORY_SEPARATOR . date('m') . DIRECTORY_SEPARATOR . date('d');
$to_path = $this->path . '/' . $date_dir;
} else {
$to_path = $this->path;
}
$upload = UploadService::init(1);
if (!file_exists($upload->uploadDir($to_path) . '/' . $name)) {
ob_start();
readfile($url);
$content = ob_get_contents();
ob_end_clean();
$size = strlen(trim($content));
if (!$content || $size <= 2) throw new ValidateException('图片流获取失败');
if ($upload->to($to_path)->down($content, $name) === false) {
throw new ValidateException('图片下载失败');
}
$imageInfo = $upload->getDownloadInfo();
$path = $imageInfo['dir'];
if ($this->thumb) {
Image::open(root_path() . 'public' . $path)->thumb($this->thumbWidth, $this->thumHeight)->save(root_path() . 'public' . $path);
$this->thumb = false;
}
} else {
$path = '/uploads/' . $to_path . '/' . $name;
$imageInfo['name'] = $name;
}
$date['path'] = $path;
$date['name'] = $imageInfo['name'];
$date['size'] = $imageInfo['size'] ?? '';
$date['mime'] = $imageInfo['type'] ?? '';
$date['image_type'] = 1;
$date['is_exists'] = false;
return $date;
}
/**
* 网络图片下载
* @param $url
* @param $name
* @param $type
* @param $timeout
* @param $w
* @param $h
* @return array|string
*/
public function downloadOnlineImage($url = '', $name = '', $type = 0, $timeout = 30, $w = 0, $h = 0)
{
if (!strlen(trim($url))) return '';
if (!strlen(trim($name))) {
//TODO 获取要下载的文件名称
$downloadImageInfo = $this->getImageExtname($url);
$ext = $downloadImageInfo['ext_name'];
$name = $downloadImageInfo['file_name'];
if (!strlen(trim($name))) throw new ValidateException('上传图片不存在');
} else {
$ext = $this->getImageExtname($name)['ext_name'];
}
if (!in_array($ext, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bpm', 'tif', 'pcx', 'tga', 'exif', 'fpx', 'svg', 'apng'])) {
$headerArray = get_headers(str_replace('\\', '/', $url), true);
if (is_array($headerArray['Content-Type']) && count($headerArray['Content-Type']) == 2) {
$contentType = $headerArray['Content-Type'][1];
} else {
$contentType = $headerArray['Content-Type'];
}
if(!$contentType) throw new ValidateException('格式错误,文件后缀不允许');
$img = explode('/',$contentType);
if($img[0]!= 'image') throw new ValidateException('格式错误,文件后缀不允许');
}
if (strstr($url, 'http://') === false && strstr($url, 'https://') === false) {
$url = 'http:' . $url;
}
$url = str_replace('https://', 'http://', $url);
if ($this->path == 'attach') {
$date_dir = date('Y') . DIRECTORY_SEPARATOR . date('m') . DIRECTORY_SEPARATOR . date('d');
$to_path = $this->path . '/' . $date_dir;
} else {
$to_path = $this->path;
}
//TODO 获取远程文件所采用的方法
if ($type) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //TODO 跳过证书检查
if (stripos($url, "https://") !== FALSE) curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); //TODO 从证书中检查SSL加密算法是否存在
curl_setopt($ch, CURLOPT_HTTPHEADER, array('user-agent:' . $_SERVER['HTTP_USER_AGENT']));
if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);//TODO 是否采集301、302之后的页面
$content = curl_exec($ch);
curl_close($ch);
} else {
try {
ob_start();
if (substr($url, 0, 2) == '//') {
$url = "https:" . $url;
}
readfile($url);
$content = ob_get_contents();
ob_end_clean();
} catch (\Exception $e) {
throw new ValidateException($e->getMessage());
}
}
$size = strlen(trim($content));
if (!$content || $size <= 2) throw new ValidateException('图片流获取失败');
$upload_type = sys_config('upload_type', 1);
$upload = UploadService::init($upload_type);
if ($upload->to($to_path)->validate()->setAuthThumb(false)->stream($content, $name) === false) {
throw new ValidateException($upload->getError());
}
$imageInfo = $upload->getUploadInfo();
$date['path'] = $imageInfo['dir'];
$date['name'] = $imageInfo['name'];
$date['size'] = $imageInfo['size'];
$date['mime'] = $imageInfo['type'];
$date['image_type'] = $upload_type;
$date['is_exists'] = false;
return $date;
}
/**
* @param $name
* @param $arguments
* @return $this
*/
public function __call($name, $arguments)
{
if (in_array($name, $this->rules)) {
if ($name === 'path') {
$this->{$name} = $arguments[0] ?? 'attach';
} else {
$this->{$name} = $arguments[0] ?? null;
}
return $this;
} else {
throw new \RuntimeException('Method does not exist' . __CLASS__ . '->' . $name . '()');
}
}
}

View File

@@ -0,0 +1,29 @@
<?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;
class ExpressService
{
protected static $api = [
'query' => 'https://wuliu.market.alicloudapi.com/kdi'
];
public static function query($no, $type = '')
{
$appCode = sys_config('system_express_app_code');
if (!$appCode) return false;
$res = HttpService::getRequest(self::$api['query'], compact('no', 'type'), ['Authorization:APPCODE ' . $appCode]);
$result = json_decode($res, true) ?: false;
return $result;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
<?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;
use FormBuilder\Factory\Iview as Form;
use FormBuilder\UI\Iview\Components\InputNumber;
/**
* Form Builder
* Class FormBuilder
* @package crmeb\services
*/
class FormBuilder extends Form
{
public static function setOptions($call)
{
if (is_array($call)) {
return $call;
} else {
return $call();
}
}
public static function number($field,$title,$value = null){
$number = new InputNumber($field, $title, $value);
if (!$number->getProp('max')) {
if ($field == 'sort') {
$number->max(99999);
if (!$number->getProp('min')) {
$number->min(0);
}
} else {
$number->max(9999999999);
}
}
return $number;
}
}

View File

@@ -0,0 +1,133 @@
<?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;
use app\services\system\config\SystemGroupDataServices;use think\facade\Config;
/**
* 获取组合数据配置
* Class GroupDataService
* @package crmeb\services
*/
class GroupDataService
{
/**
* 缓存前缀字符
*/
const CACHE_SYSTEM = 'system_group_data';
/**
* 过期时间
*/
const EXPIRE_TIME = 30 * 24 * 3600;
/**
* 获取配置缓存前缀
* @return string
*/
public static function getTag()
{
return Config::get('cache.stores.redis.tag_prefix') . 'cache_' . self::CACHE_SYSTEM;
}
/**
* 获取单个值
* @param string $config_name 配置名称
* @param int $limit 截取多少条
* @param bool $isCaChe 是否读取缓存
* @return array
*/
public static function getData(string $config_name, int $limit = 0, bool $isCaChe = false): array
{
$callable = function () use ($config_name, $limit) {
event('get.config');
try {
/** @var SystemGroupDataServices $service */
$service = app()->make(SystemGroupDataServices::class);
return $service->getConfigNameValue($config_name, $limit);
} catch (\Exception) {
return [];
}
};
try {
$cacheName = $limit ? "data_{$config_name}_{$limit}" : "data_{$config_name}";
if ($isCaChe)
return $callable();
return CacheService::redisHandler(self::getTag())->remember($cacheName, $callable, self::EXPIRE_TIME);
} catch (\Throwable) {
try {
return $callable();
} catch (\Throwable) {
return [];
}
}
}
/**
* 根据id 获取单个值
* @param int $id
* @param bool $isCaChe 是否读取缓存
* @return array
*/
public static function getDataNumber(int $id, bool $isCaChe = false): array
{
$callable = function () use ($id) {
try {
/** @var SystemGroupDataServices $service */
$service = app()->make(SystemGroupDataServices::class);
$data = $service->getDateValue($id);
if (is_object($data))
$data = $data->toArray();
return $data;
} catch (\Exception) {
return [];
}
};
try {
$cacheName = "data_number_{$id}";
if ($isCaChe)
return $callable();
return CacheService::redisHandler(self::getTag())->remember($cacheName, $callable, self::EXPIRE_TIME);
} catch (\Throwable) {
try {
return $callable();
} catch (\Throwable) {
return [];
}
}
}
/**
* 清空配置缓存
* @return bool|void
*/
public static function clear()
{
try {
return CacheService::redisHandler(self::getTag())->clear();
} catch (\Throwable $e) {
\think\facade\Log::error('清空配置缓存失败:原因:' . $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,156 @@
<?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;
/**
* Class HttpService
* @package crmeb\services
*/
class HttpService
{
/**
* 错误信息
* @var string|null
*/
private static string|null $curlError;
/**
* header头信息
* @var string|null
*/
private static string|null $headerStr;
/**
* 请求状态
* @var int
*/
private static $status;
/**
* @return string
*/
public static function getCurlError()
{
return self::$curlError;
}
/**
* @return mixed
*/
public static function getStatus()
{
return self::$status;
}
/**
* 模拟GET发起请求
* @param $url
* @param array $data
* @param bool $header
* @param int $timeout
* @return bool|string
*/
public static function getRequest($url, $data = array(), $header = false, $timeout = 10)
{
if (!empty($data)) {
$url .= (stripos($url, '?') === false ? '?' : '&');
$url .= (is_array($data) ? http_build_query($data) : $data);
}
return self::request($url, 'get', array(), $header, $timeout);
}
/**
* curl 请求
* @param $url
* @param string $method
* @param array $data
* @param bool $header
* @param int $timeout
* @return bool|string
*/
public static function request($url, $method = 'get', $data = array(), $header = false, $timeout = 15)
{
self::$status = null;
self::$curlError = null;
self::$headerStr = null;
$curl = curl_init($url);
$method = strtoupper($method);
//请求方式
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
//post请求
if ($method == 'POST') {
curl_setopt($curl, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
} elseif ($method == 'GET' && count($data)) {
$url .= '?' . http_build_query($data);
curl_setopt($curl, CURLOPT_URL, $url);
}
//超时时间
curl_setopt($curl, CURLOPT_TIMEOUT, 300);
//设置header头
if ($header !== false) curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_FAILONERROR, false);
//返回抓取数据
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
//输出header头信息
curl_setopt($curl, CURLOPT_HEADER, true);
//TRUE 时追踪句柄的请求字符串,从 PHP 5.1.3 开始可用。这个很关键,就是允许你查看请求header
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
//https请求
if (1 == strpos("$" . $url, "https://")) {
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
}
self::$curlError = curl_error($curl);
[$content, $status] = [curl_exec($curl), curl_getinfo($curl), curl_close($curl)];
self::$status = $status;
self::$headerStr = trim(substr($content, 0, $status['header_size']));
$content = trim(substr($content, $status['header_size']));
return (intval($status["http_code"]) === 200) ? $content : false;
}
/**
* 模拟POST发起请求
* @param $url
* @param array $data
* @param bool $header
* @param int $timeout
* @return bool|string
*/
public static function postRequest($url, $data = [], $header = false, $timeout = 10)
{
return self::request($url, 'post', $data, $header, $timeout);
}
/**
* 获取header头字符串类型
* @return mixed
*/
public static function getHeaderStr()
{
return self::$headerStr;
}
/**
* 获取header头数组类型
* @return array
*/
public static function getHeader()
{
$headArr = explode("\r\n", self::$headerStr);
return $headArr;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace crmeb\services;
use think\exception\ValidateException;
use think\facade\Cache;
class LockService
{
/**
* @param $key
* @param $fn
* @param int $ex
* @return mixed
* @author 吴汐
* @email 442384644@qq.com
* @date 2023/03/01
*/
public function exec($key, $fn, int $ex = 6)
{
try {
if ($this->lock($key, $key, $ex))
return $fn();
else throw new ValidateException('请求太过频繁,请稍后再试');
} finally {
$this->unlock($key, $key);
}
}
public function tryLock($key, $value = '1', $ex = 6)
{
return Cache::store('redis')->handler()->set('lock_' . $key, $value, ["NX", "EX" => $ex]);
}
public function lock($key, $value = '1', $ex = 6)
{
if ($this->tryLock($key, $value, $ex)) {
return true;
} else {
return false;
}
}
public function unlock($key, $value = '1')
{
$script = <<< EOF
if (redis.call("get", "lock_" .. KEYS[1]) == ARGV[1]) then
return redis.call("del", "lock_" .. KEYS[1])
else
return 0
end
EOF;
return Cache::store('redis')->handler()->eval($script, [$key, $value], 1) > 0;
}
}

View File

@@ -0,0 +1,526 @@
<?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;
use think\facade\Db;
class MysqlBackupService
{
/**
* 文件指针
* @var resource
*/
private $fp;
/**
* 备份文件信息 part - 卷号,name - 文件名
* @var array
*/
private $file;
/**
* 当前打开文件大小
* @var integer
*/
private $size = 0;
/**
* 数据库配置
* @var integer
*/
private $dbconfig = array();
/**
* 备份配置
* @var integer
*/
private $config = array(
'path' => './Data/',
//数据库备份路径
'part' => 20971520,
//数据库备份卷大小
'compress' => 0,
//数据库备份文件是否启用压缩 0不压缩 1 压缩
'level' => 9,
);
/**
* 数据库备份构造方法
*
* @param array $file 备份或还原的文件信息
* @param array $config 备份配置信息
*/
public function __construct($config = [])
{
$this->config['path'] = app()->getRootPath() . 'backup/';
$this->config = array_merge($this->config, $config);
//初始化文件名
$this->setFile();
//初始化数据库连接参数
$this->setDbConn();
//检查文件是否可写
if (!$this->checkPath($this->config['path'])) {
throw new \Exception("The current directory is not writable");
}
}
/**
* 设置脚本运行超时时间
* 0表示不限制,支持连贯操作
*/
public function setTimeout($time = null)
{
if (!is_null($time)) {
set_time_limit($time) || ini_set("max_execution_time", $time);
}
return $this;
}
/**
* 设置数据库连接必备参数
*
* @param array $dbconfig 数据库连接配置信息
* @return $this
*/
public function setDbConn($dbconfig = [])
{
if (empty($dbconfig)) {
$this->dbconfig = config('database.connections.' . config('database.default'));
//$this->dbconfig = Config::get('database');
} else {
$this->dbconfig = $dbconfig;
}
return $this;
}
/**
* 设置备份文件名
*
* @param null $file
* @return $this
*/
public function setFile($file = null)
{
if (is_null($file)) {
$this->file = ['name' => date('Ymd-His'), 'part' => 1];
} else {
if (!array_key_exists("name", $file) && !array_key_exists("part", $file)) {
$this->file = $file['1'];
} else {
$this->file = $file;
}
}
return $this;
}
//数据类连接
public static function connect()
{
return Db::connect();
}
/**
* 数据库表列表
*
* @param null $table
* @param int $type
* @return array
* @throws \think\db\exception\BindParamException
* @throws \think\exception\PDOException
*/
public function dataList(?string $table = null, int $type = 1)
{
$key = 'mysql_backup_' . $table . '_' . $type;
$data = CacheService::get($key);
if (!$data) {
$db = self::connect();
if (is_null($table)) {
$list = $db->query("SHOW TABLE STATUS");
} else {
if ($type) {
$list = $db->query("SHOW FULL COLUMNS FROM {$table}");
} else {
$list = $db->query("show columns from {$table}");
}
}
$data = array_map('array_change_key_case', $list);
CacheService::set($key, $data, 7200);
}
return $data;
}
/**
* 数据库备份文件列表
*
* @return array
*/
public function fileList()
{
$list = [];
if (!is_dir($this->config['path'])) {
mkdir($this->config['path'], 0755, true);
return $list;
}
$path = realpath($this->config['path']);
$flag = \FilesystemIterator::KEY_AS_FILENAME;
$glob = new \FilesystemIterator($path, $flag);
foreach ($glob as $name => $file) {
if (preg_match('/^\\d{8,8}-\\d{6,6}-\\d+\\.sql(?:\\.gz)?$/', $name)) {
$info['filename'] = $name;
$name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d');
$date = $name[0] . '-' . $name[1] . '-' . $name[2];
$time = $name[3] . ':' . $name[4] . ':' . $name[5];
$part = $name[6];
if (isset($list[$date . $time])) {
$info = $list[$date . $time];
$info['part'] = max($info['part'], $part);
$info['size'] = $info['size'] + $file->getSize();
} else {
$info['part'] = $part;
$info['size'] = $file->getSize();
}
$extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
$info['compress'] = $extension === 'SQL' ? '-' : $extension;
$info['time'] = strtotime($date . $time);
$list[$date . $time] = $info;
}
}
return $list;
}
/**
* @param string $type
* @param int $time
* @return array|false|string
* @throws \Exception
*/
public function getFile($type = '', $time = 0)
{
//
if (!is_numeric($time)) {
throw new \Exception("{$time} Illegal data type");
}
switch ($type) {
case 'time':
$name = date('Ymd-His', $time) . '-*.sql*';
$path = realpath($this->config['path']) . DIRECTORY_SEPARATOR . $name;
return glob($path);
break;
case 'timeverif':
$name = date('Ymd-His', $time) . '-*.sql*';
$path = realpath($this->config['path']) . DIRECTORY_SEPARATOR . $name;
$files = glob($path);
$list = array();
foreach ($files as $name) {
$basename = basename($name);
$match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d');
$gz = preg_match('/^\\d{8,8}-\\d{6,6}-\\d+\\.sql.gz$/', $basename);
$list[$match[6]] = array($match[6], $name, $gz);
}
$last = end($list);
if (count($list) === $last[0]) {
return $list;
} else {
throw new \Exception("File {$files['0']} may be damaged, please check again");
}
break;
case 'pathname':
return "{$this->config['path']}{$this->file['name']}-{$this->file['part']}.sql";
break;
case 'filename':
return "{$this->file['name']}-{$this->file['part']}.sql";
break;
case 'filepath':
return $this->config['path'];
break;
default:
$arr = array('pathname' => "{$this->config['path']}{$this->file['name']}-{$this->file['part']}.sql", 'filename' => "{$this->file['name']}-{$this->file['part']}.sql", 'filepath' => $this->config['path'], 'file' => $this->file);
return $arr;
}
}
/**
* 删除备份文件
* @param $time
* @return mixed
* @throws \Exception
*/
public function delFile($time)
{
if ($time) {
$file = $this->getFile('time', $time);
array_map("unlink", $this->getFile('time', $time));
if (count($this->getFile('time', $time))) {
throw new \Exception("File {$time} deleted failed");
} else {
return $time;
}
} else {
throw new \Exception("{$time} Time parameter is incorrect");
}
}
/**
* 下载备份
*
* @param $time
* @param int $part
* @throws \Exception
*/
public function downloadFile($time, int $part = 0, bool $isFile = false)
{
$file = $this->getFile('time', $time);
$fileName = $file[$part];
if (file_exists($fileName)) {
if ($isFile) {
$key = password_hash(time() . $fileName, PASSWORD_DEFAULT);
CacheService::set($key, ['path' => $fileName, 'fileName' => substr(strstr($fileName, 'backup'), 7)], 300);
return $key;
}
ob_end_clean();
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header('Content-Description: File Transfer');
header('Access-Control-Allow-Origin: ' . request()->domain());
header('Content-Type: application/octet-stream');
header('Content-Length: ' . filesize($fileName));
header('Content-Disposition: attachment; filename=' . basename($fileName));
return readfile($fileName);
} else {
throw new \Exception("{$time} File is abnormal");
}
}
public function import($start)
{
//还原数据
$db = self::connect();
if ($this->config['compress']) {
$gz = gzopen($this->file[1], 'r');
$size = 0;
} else {
$size = filesize($this->file[1]);
$gz = fopen($this->file[1], 'r');
}
$sql = '';
if ($start) {
$this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start);
}
for ($i = 0; $i < 1000; $i++) {
$sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz);
if (preg_match('/.*;$/', trim($sql))) {
if (false !== $db->execute($sql)) {
$start += strlen($sql);
} else {
return false;
}
$sql = '';
} elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) {
return 0;
}
}
return array($start, $size);
}
/**
* 写入初始数据
*
* @return boolean true - 写入成功,false - 写入失败
*/
public function Backup_Init()
{
$sql = "-- -----------------------------\n";
$sql .= "-- Think MySQL Data Transfer \n";
$sql .= "-- \n";
$sql .= "-- Host : " . $this->dbconfig['hostname'] . "\n";
$sql .= "-- Port : " . $this->dbconfig['hostport'] . "\n";
$sql .= "-- Database : " . $this->dbconfig['database'] . "\n";
$sql .= "-- \n";
$sql .= "-- Part : #{$this->file['part']}\n";
$sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n";
$sql .= "-- -----------------------------\n\n";
$sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
return $this->write($sql);
}
/**
* 备份表结构
*
* @param string $table
* @param int $start
* @return bool|int
* @throws \think\db\exception\BindParamException
* @throws \think\exception\PDOException
*/
public function backup(string $table, int $start, $sql = '')
{
$db = self::connect();
// 备份表结构
if (0 == $start) {
$result = $db->query("SHOW CREATE TABLE `{$table}`");
$sql .= "\n";
$sql .= "-- -----------------------------\n";
$sql .= "-- Table structure for `{$table}`\n";
$sql .= "-- -----------------------------\n";
$sql .= "DROP TABLE IF EXISTS `{$table}`;\n";
$sql .= trim($result[0]['Create Table']) . ";\n\n";
}
//数据总数
$result = $db->query("SELECT COUNT(*) AS count FROM `{$table}`");
$count = $result['0']['count'];
//备份表数据
if ($count) {
//写入数据注释
if (0 == $start) {
$sql .= "-- -----------------------------\n";
$sql .= "-- Records of `{$table}`\n";
$sql .= "-- -----------------------------\n";
}
//备份数据记录
$result = $db->query("SELECT * FROM `{$table}` LIMIT :MIN, 1000", ['MIN' => intval($start)]);
foreach ($result as $row) {
$row = array_map('addslashes', $row);
$sql .= "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r", "\n"), array('\\r', '\\n'), implode("', '", $row)) . "');\n";
}
if (false === $this->write($sql)) {
return false;
}
//还有更多数据
if ($count > $start + 1000) {
//return array($start + 1000, $count);
return $this->backup($table, $start + 1000);
}
}
//备份下一表
return 0;
}
/**
* 优化表
*
* @param array|string $tables
* @throws \think\db\exception\BindParamException
* @throws \think\exception\PDOException
*/
public function optimize($tables)
{
if ($tables) {
$db = self::connect();
if (is_array($tables)) {
$tables = implode('`,`', $tables);
$list = $db->query("OPTIMIZE TABLE `{$tables}`");
} else {
$list = $db->query("OPTIMIZE TABLE {$tables}");
}
if (!$list) {
throw new \Exception("data sheet'{$tables}'Repair mistakes please try again!");
}
return $list;
} else {
throw new \Exception("Please specify the table to be repaired!");
}
}
/**
* 修复表
*
* @param string|null $tables
* @return array
* @throws \think\db\exception\BindParamException
* @throws \think\exception\PDOException
*/
public function repair(?string $tables = null)
{
if ($tables) {
$db = self::connect();
if (is_array($tables)) {
$tables = implode('`,`', $tables);
$list = $db->query("REPAIR TABLE `{$tables}`");
} else {
$list = $db->query("REPAIR TABLE {$tables}");
}
if ($list) {
return $list;
} else {
throw new \Exception("data sheet'{$tables}'Repair mistakes please try again!");
}
} else {
throw new \Exception("Please specify the table to be repaired!");
}
}
/**
* 写入SQL语句
*
* @param string $sql 要写入的SQL语句
* @return boolean true - 写入成功,false - 写入失败!
*/
private function write(string $sql)
{
$size = strlen($sql);
//由于压缩原因,无法计算出压缩后的长度,这里假设压缩率为50%,
//一般情况压缩率都会高于50%;
$size = $this->config['compress'] ? $size / 2 : $size;
$this->open($size);
return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql);
}
/**
* 打开一个卷,用于写入数据
*
* @param integer $size 写入数据的大小
*/
private function open(int $size)
{
if ($this->fp) {
$this->size += $size;
if ($this->size > $this->config['part']) {
$this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
$this->fp = null;
$this->file['part']++;
session('backup_file', $this->file);
$this->Backup_Init();
}
} else {
$backuppath = $this->config['path'];
$filename = "{$backuppath}{$this->file['name']}-{$this->file['part']}.sql";
if ($this->config['compress']) {
$filename = "{$filename}.gz";
$this->fp = @gzopen($filename, "a{$this->config['level']}");
} else {
$this->fp = @fopen($filename, 'a');
}
$this->size = filesize($filename) + $size;
}
}
/**
* 检查目录是否可写
*
* @param string $path
* @return bool
*/
protected function checkPath(string $path)
{
if (is_dir($path)) {
return true;
}
if (mkdir($path, 0755, true)) {
return true;
} else {
return false;
}
}
/**
* 析构方法,用于关闭文件资源
*/
public function __destruct()
{
$this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
}
}

View File

@@ -0,0 +1,120 @@
<?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;
use app\services\system\attachment\SystemAttachmentServices;
use crmeb\services\wechat\MiniProgram;
use GuzzleHttp\Psr7\Utils;
/**
* Class QrcodeService
* @package crmeb\services
*/
class QrcodeService
{
/**
* 获取二维码完整路径,不存在则自动生成
* @param string $name 路径名
* @param string $link 需要生成二维码的跳转路径
* @param int $type https 1 = http , 0 = https
* @param bool $force 是否返回false
* @return bool|mixed|string
*/
public static function getWechatQrcodePath(string $name, string $link, bool $force = false)
{
/** @var SystemAttachmentServices $systemAttchment */
$systemAttchment = app()->make(SystemAttachmentServices::class);
try {
$imageInfo = $systemAttchment->getInfo(['name' => $name]);
$siteUrl = sys_config('site_url');
if (!$imageInfo) {
$codeUrl = UtilService::setHttpType($siteUrl . $link, request()->isSsl() ? 0 : 1);//二维码链接
$imageInfo = UtilService::getQRCodePath($codeUrl, $name);
if (is_string($imageInfo) && $force)
return false;
if (is_array($imageInfo)) {
$systemAttchment->attachmentAdd($imageInfo['name'], $imageInfo['size'], $imageInfo['type'], $imageInfo['dir'], $imageInfo['thumb_path'], 1, $imageInfo['image_type'], $imageInfo['time'], 2);
$url = $imageInfo['dir'];
} else {
$url = '';
$imageInfo = ['image_type' => 0];
}
} else $url = $imageInfo['att_dir'];
if ($imageInfo['image_type'] == 1 && $url) $url = $siteUrl . $url;
return $url;
} catch (\Throwable $e) {
if ($force)
return false;
else
return '';
}
}
/**
* 获取小程序分享二维码
* @param int $id
* @param int $uid
* @param int $type 1 = 拼团,2 = 秒杀
* @return bool|string
*/
public static function getRoutineQrcodePath(int $id, int $uid, int $type, array $parame = [])
{
$page = '';
$namePath = '';
$data = 'id=' . $id . '&pid=' . $uid;
switch ($type) {
case 1:
$page = '/pages/activity/goods_details/index?type=3';
$namePath = 'combination_' . $id . '_' . $uid . '.jpg';
break;
case 2:
$page = '/pages/activity/goods_details/index?type=1';
$namePath = 'seckill_' . $id . '_' . $uid . '.jpg';
if (isset($parame['stop_time']) && $parame['stop_time']) {
$data .= '&time=' . $parame['stop_time'];
$namePath = $parame['stop_time'] . $namePath;
}
break;
}
if (!$page || !$namePath) {
return false;
}
/** @var SystemAttachmentServices $systemAttchment */
$systemAttchment = app()->make(SystemAttachmentServices::class);
try {
$imageInfo = $systemAttchment->getInfo($namePath, 'name');
$siteUrl = sys_config('site_url');
if (!$imageInfo) {
$res = MiniProgram::appCodeUnlimit($page, $data, 280);
if (!$res) return false;
$uploadType = (int)sys_config('upload_type', 1);
$upload = UploadService::init($uploadType);
$res = (string)Utils::streamFor($res);
$res = $upload->to('routine/product')->validate()->stream($res, $namePath);
if ($res === false) {
return false;
}
$imageInfo = $upload->getUploadInfo();
$imageInfo['image_type'] = $uploadType;
if ($imageInfo['image_type'] == 1) $remoteImage = UtilService::remoteImage($siteUrl . $imageInfo['dir']);
else $remoteImage = UtilService::remoteImage($imageInfo['dir']);
if (!$remoteImage['status']) return false;
$systemAttchment->attachmentAdd($imageInfo['name'], $imageInfo['size'], $imageInfo['type'], $imageInfo['dir'], $imageInfo['thumb_path'], 1, $imageInfo['image_type'], $imageInfo['time'], 2);
$url = $imageInfo['dir'];
} else $url = $imageInfo['att_dir'];
if ($imageInfo['image_type'] == 1) $url = $siteUrl . $url;
return $url;
} catch (\Throwable $e) {
return false;
}
}
}

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;
use think\facade\Config;
use Swoole\Coroutine\Channel;
use Swoole\Coroutine\Redis;
/**
* Class RedisService
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/29
* @package crmeb\services
*/
class RedisService
{
/**
* @var array
*/
protected $config;
/**
* 获取进程池最大时间
* @var int
*/
protected $maxWaitTime = 3;
/**
* 最大连接池数量
* @var int
*/
protected $maxActive;
/**
* @var Channel
*/
protected $pool;
/**
* RedisService constructor.
* @param array $config
* @param array $pool
*/
public function __construct(array $config = [], array $pool = [])
{
$this->config = $config ?: Config::get('cache.stores.redis');
$this->maxActive = $pool['maxActive'] ?? Config::get('swoole.pool.redis.cache.max_active', 50);
$this->maxWaitTime = $pool['maxWaitTime'] ?? Config::get('swoole.pool.redis.cache.max_wait_time', 5);
}
/**
* 初始化连接池
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/29
*/
public function init()
{
$this->pool = new Channel($this->maxActive);
go(function () {
for ($i = 0; $i < $this->maxActive; $i++) {
$redis = $this->createConnect($this->config);
$this->pool->push($redis);
}
});
return $this;
}
/**
* 创建连接
* @param array $config
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/29
*/
public function createConnect(array $config)
{
$connection = new Redis();
$res = $connection->connect($config['host'], $config['port'], \Redis::SERIALIZER_PHP);
if ($config['password']) {
$connection->auth($config['password']);
}
$connection->select($config['select']);
if ($res === false) {
throw new \RuntimeException("Failed to connect redis server");
}
return $connection;
}
/**
* 销毁连接
* @param $connection
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/29
*/
public function removeConnect($connection)
{
\Swoole\Coroutine\defer(function () use ($connection) { //释放
$connection->close();
});
}
/**
* 获取redis连接句柄
* @return Redis
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/29
*/
public function getRedis()
{
if ($this->pool->isEmpty()) {
$redis = $this->createConnect($this->config);
$this->removeConnect($redis);
return $redis;
}
$redis = $this->pool->pop($this->maxWaitTime);
if (false === $redis) {
$redis = $this->createConnect($this->config);
$this->removeConnect($redis);
} else {
\Swoole\Coroutine\defer(function () use ($redis) { //释放
$this->pool->push($redis);
});
}
return $redis;
}
/**
* @param $name
* @param $arguments
* @return Redis
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/29
*/
public function __call($name, $arguments)
{
return call_user_func_array([$this->getRedis(), $name], $arguments);
}
}

View File

@@ -0,0 +1,344 @@
<?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;
use crmeb\utils\Arr;
use PhpOffice\PhpSpreadsheet\IOFactory;
class SpreadsheetExcelService
{
//
private static $instance = null;
//PHPSpreadsheet实例化对象
private static $spreadsheet = null;
//sheet实例化对象
private static $sheet = null;
//表头计数
protected static $count;
//表头占行数
protected static $topNumber = 3;
//表能占据表行的字母对应self::$cellkey
protected static $cells;
//表头数据
protected static $data = [];
//文件名
protected static $title = '订单导出';
//行宽
protected static $width = 20;
//行高
protected static $height = 50;
//保存文件目录
protected static $path = '/phpExcel/';
//设置style
private static $styleArray = [
// 'borders' => [
// 'allBorders' => [
// // PHPExcel_Style_Border里面有很多属性,想要其他的自己去看
// // 'style' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THICK,//边框是粗的
// // 'style' => \PHPExcel_Style_Border::BORDER_DOUBLE,//双重的
// // 'style' => \PHPExcel_Style_Border::BORDER_HAIR,//虚线
// // 'style' => \PHPExcel_Style_Border::BORDER_MEDIUM,//实粗线
// // 'style' => \PHPExcel_Style_Border::BORDER_MEDIUMDASHDOT,//虚粗线
// // 'style' => \PHPExcel_Style_Border::BORDER_MEDIUMDASHDOTDOT,//点虚粗线
// 'style' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,//细边框
// // 'color' => ['argb' => 'FFFF0000'],
// ],
// ],
'font' => [
'bold' => true
],
'alignment' => [
'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER
]
];
private function __construct()
{
}
private function __clone()
{
}
public static function instance()
{
if (self::$instance === null) {
self::$instance = new self();
self::$spreadsheet = $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
self::$sheet = $spreadsheet->getActiveSheet();
}
return self::$instance;
}
/**
* 设置字符串字符集
* @param string $str 需要设置字符集的字符串
* @return string
*/
public function utf8String($str)
{
$str = is_object($str) ? $str->__toString() : $str;
$encode = mb_detect_encoding($str, ["ASCII", 'UTF-8', "GB2312", "GBK", 'BIG5', 'CP936']);
if (strtoupper($encode) != 'UTF-8') $str = mb_convert_encoding($str, 'utf-8', $encode);
return $str;
}
/**
* 创建保存excel目录
* return string
*/
public static function savePath()
{
if (!is_dir(self::$path)) {
if (mkdir(self::$path, 0700) == false) {
return false;
}
}
//年月一级目录
$mont_path = self::$path . date('Ym');
if (!is_dir($mont_path)) {
if (mkdir($mont_path, 0700) == false) {
return false;
}
}
//日二级目录
$day_path = $mont_path . '/' . date('d');
if (!is_dir($day_path)) {
if (mkdir($day_path, 0700) == false) {
return false;
}
}
return $day_path;
}
/**
* 设置标题
* @param $title string || array ['title'=>'','name'=>'','info'=>[]]
* @param $Name string
* @param $info string || array;
* @param $funName function($style,$A,$A2) 自定义设置头部样式
* @return $this
*/
public function setExcelTile($title = '', $Name = '', $info = [], $funName = null)
{
//设置参数
if (is_array($title)) {
if (isset($title['title'])) $title = $title['title'];
if (isset($title['name'])) $Name = $title['name'];
if (isset($title['info'])) $info = $title['info'];
}
if (empty($title))
$title = self::$title;
else
self::$title = $title;
if (empty($Name)) $Name = time();
//设置Excel属性
self::$spreadsheet->getProperties()
->setCreator("Neo")
->setLastModifiedBy("Neo")
->setTitle($this->utf8String($title))
->setSubject($Name)
->setDescription("")
->setKeywords($Name)
->setCategory("");
self::$sheet->setTitle($Name);
self::$sheet->setCellValue('A1', $title);
self::$sheet->setCellValue('A2', self::setCellInfo($info));
//文字居中
self::$sheet->getStyle('A1')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
self::$sheet->getStyle('A2')->getAlignment()->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER);
//合并表头单元格
self::$sheet->mergeCells('A1:' . self::$cells . '1');
self::$sheet->mergeCells('A2:' . self::$cells . '2');
self::$sheet->getRowDimension(1)->setRowHeight(40);
self::$sheet->getRowDimension(2)->setRowHeight(20);
//设置表头字体
self::$sheet->getStyle('A1')->getFont()->setName('黑体');
self::$sheet->getStyle('A1')->getFont()->setSize(20);
self::$sheet->getStyle('A1')->getFont()->setBold(true);
self::$sheet->getStyle('A2')->getFont()->setName('宋体');
self::$sheet->getStyle('A2')->getFont()->setSize(14);
self::$sheet->getStyle('A2')->getFont()->setBold(true);
self::$sheet->getStyle('A3:' . self::$cells . '3')->getFont()->setBold(true);
return $this;
}
/**
* 设置第二行标题内容
* @param $info array (['name'=>'','site'=>'','phone'=>123] || ['我是表名','我是地址','我是手机号码'] ) || string 自定义
* @return string
*/
private static function setCellInfo($info)
{
$content = ['操作者:', '导出日期:' . date('Y-m-d', time()), '地址:', '电话:'];
if (is_array($info) && !empty($info)) {
if (isset($info['name'])) {
$content[0] .= $info['name'];
} else {
$content[0] .= isset($info[0]) ? $info[0] : '';
}
if (isset($info['site'])) {
$content[2] .= $info['site'];
} else {
$content[2] .= isset($info[1]) ? $info[1] : '';
}
if (isset($info['phone'])) {
$content[3] .= $info['phone'];
} else {
$content[3] .= isset($info[2]) ? $info[2] : '';
}
return implode(' ', $content);
} else if (is_string($info)) {
return empty($info) ? implode(' ', $content) : $info;
}
}
/**
* 设置头部信息
* @param $data array
* @return $this
*/
public function setExcelHeader($data)
{
$span = 'A';
foreach ($data as $key => $value) {
self::$sheet->getColumnDimension($span)->setWidth(self::$width);
self::$sheet->setCellValue($span . self::$topNumber, $value);
$span++;
}
self::$sheet->getRowDimension(3)->setRowHeight(self::$height);
self::$cells = $span;
return $this;
}
/**
* 读取表格内的文件数据
* @param string $filePath
* @param array $cellsData
* @param callable $closure
* @param int $startLine
* @return array
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
*/
public static function getExcelData(string $filePath, array $cellsData = [], callable $closure = null, int $startLine = 2)
{
if (!file_exists($filePath)) {
throw new \RuntimeException('文件不存在');
}
$extension = ucwords(pathinfo($filePath, PATHINFO_EXTENSION));
$io = IOFactory::createreader($extension);
$spreadsheet = $io->load($filePath);
$worksheet = $spreadsheet->getActiveSheet();
$highestRow = $worksheet->getHighestRow();
$data = [];
if ($closure) {
$closure($worksheet);
}
for ($j = $startLine; $j <= (int)$highestRow; $j++) {
$value = [];
foreach ($cellsData as $key => $val) {
if ($v = $worksheet->getCell($val . $j)->getValue()) {
$value[$key] = $v;
}
}
if ($value) {
$data[] = $value;
}
}
return Arr::filterValue($data);
}
/**
*
* execl数据导出
* @param $data 需要导出的数据 格式和以前的可是一样
*
* 特殊处理:合并单元格需要先对数据进行处理
*/
public function setExcelContent($data = [])
{
if (!empty($data) && is_array($data)) {
$column = self::$topNumber + 1;
// 行写入
foreach ($data as $key => $rows) {
$span = 'A';
// 列写入
foreach ($rows as $keyName => $value) {
self::$sheet->setCellValue($span . $column, $this->utf8String($value));
$span++;
}
$column++;
}
self::$sheet->getDefaultRowDimension()->setRowHeight(self::$height);
//设置内容字体样式
self::$sheet->getStyle('A1:' . $span . $column)->applyFromArray(self::$styleArray);
//设置边框
self::$sheet->getStyle('A1:' . $span . ($column - 1))->getBorders()->getAllBorders()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
//设置自动换行
self::$sheet->getStyle('A4:' . $span . $column)->getAlignment()->setWrapText(true);
}
return $this;
}
/**
* 保存表格数据,直接下载
* @param $filename 文件名称
* @param $suffix 文件后缀名
* @param $is_save 是否保存文件
* @return 保存文件:return string
*/
public function excelSave($fileName = '', $suffix = 'xlsx', $is_save = false)
{
if (empty($fileName)) {
$fileName = date('YmdHis') . time();
}
if (empty($suffix)) {
$suffix = 'xlsx';
}
// 重命名表(UTF8编码不需要这一步)
if (mb_detect_encoding($fileName) != "UTF-8") {
$fileName = iconv("utf-8", "gbk//IGNORE", $fileName);
}
$spreadsheet = self::$spreadsheet;
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
if (!$is_save) {//直接下载
header('Content-Disposition: attachment;filename="' . $fileName . '.' . $suffix . '"');
header('Cache-Control: max-age=0');
$writer->save('php://output');
// 删除清空 释放内存
// $spreadsheet->disconnectWorksheets();
unset($spreadsheet);
} else {//保存文件
$root_path = app()->getRootPath() . 'public/phpExcel';
if (!is_dir($root_path)) mkdir($root_path, 0700, true);
$path = $root_path . '/' . $fileName . '.' . $suffix;
//$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx');
//$writer->save($path);
$writer->save($path);
// 删除清空 释放内存
// $spreadsheet->disconnectWorksheets();
unset($spreadsheet);
return '/phpExcel/' . $fileName . '.' . $suffix;
}
}
}

View File

@@ -0,0 +1,146 @@
<?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;
use app\services\system\config\SystemConfigServices;
use crmeb\utils\Arr;
use think\facade\Config;
use think\facade\Db;
/** 获取系统配置服务类
* Class SystemConfigService
* @package service
*/
class SystemConfigService
{
/**
* 缓存前缀字符
*/
const CACHE_SYSTEM = 'system_config';
/**
* 过期时间
*/
const EXPIRE_TIME = 30 * 24 * 3600;
/**
* @var int
*/
protected static $relationId = 0;
/**
* @var int
*/
protected static $type = 0;
/**
* 获取配置缓存前缀
* @return string
*/
public static function getTag()
{
return Config::get('cache.stores.redis.tag_prefix') . 'cache_' . self::CACHE_SYSTEM;
}
/**
* @param int $storeId
*/
public static function setStore(int $storeId)
{
self::$relationId = $storeId;
self::$type = 1;
}
/**
* @param int $supplier
*/
public static function setSupplier(int $supplier)
{
self::$relationId = $supplier;
self::$type = 2;
}
/**
* 获取单个配置效率更高
* @param $key
* @param string $default
* @param bool $isCaChe 是否获取缓存配置
* @return bool|mixed|string
*/
public static function get(string $key, $default = '', bool $isCaChe = false)
{
$cacheName = self::CACHE_SYSTEM . ':' . $key . (self::$type ? '_' . self::$type : '') . (self::$relationId ? '_' . self::$relationId : '');
$type = self::$type;
$relationId = self::$relationId;
$callable = function () use ($key, $type, $relationId) {
event('get.config');
/** @var SystemConfigServices $service */
$service = app()->make(SystemConfigServices::class);
return $service->getConfigValue($key, $type, $relationId);
};
try {
if ($isCaChe) {
return $callable();
}
$value = CacheService::redisHandler(self::getTag())->remember($cacheName, $callable, self::EXPIRE_TIME);
self::$relationId = 0;
self::$type = 0;
return $value;
} catch (\Throwable $e) {
return $default;
}
}
/**
* 获取多个配置
* @param array $keys 示例 [['appid','1'],'appkey']
* @param bool $isCaChe 是否获取缓存配置
* @return array
*/
public static function more(array $keys, bool $isCaChe = false)
{
$cacheName = self::CACHE_SYSTEM . ':' . md5(implode(',', $keys) . (self::$type ? '_' . self::$type : '') . (self::$relationId ? '_' . self::$relationId : ''));
$type = self::$type;
$relationId = self::$relationId;
$callable = function () use ($keys, $type, $relationId) {
/** @var SystemConfigServices $service */
$service = app()->make(SystemConfigServices::class);
return Arr::getDefaultValue($keys, $service->getConfigAll($keys, $type, $relationId));
};
try {
if ($isCaChe)
return $callable();
$value = CacheService::redisHandler(self::getTag())->remember($cacheName, $callable, self::EXPIRE_TIME);
self::$relationId = 0;
self::$type = 0;
return $value;
} catch (\Throwable $e) {
return Arr::getDefaultValue($keys);
}
}
/**
* 清空配置缓存
* @return bool|void
*/
public static function clear()
{
try {
return CacheService::redisHandler(self::getTag())->clear();
} catch (\Throwable $e) {
\think\facade\Log::error('清空配置缓存失败:原因:' . $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,148 @@
<?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;
use app\services\system\config\SystemStorageServices;
use crmeb\services\upload\Upload;
/**
* Class UploadService
* @package crmeb\services
*/
class UploadService
{
protected static $upload = [];
/**
* @param null $type
* @return Upload|mixed
*/
public static function init($type = null, $is_cache = false)
{
if (is_null($type)) {
$type = (int)sys_config('upload_type', 1);
}
if ($is_cache && isset(self::$upload['upload_' . $type]) && self::$upload['upload_' . $type]) {
return self::$upload['upload_' . $type];
}
$type = (int)$type;
$config = [];
switch ($type) {
case 2://七牛
$config = [
'accessKey' => sys_config('qiniu_accessKey'),
'secretKey' => sys_config('qiniu_secretKey'),
];
break;
case 3:// oss 阿里云
$config = [
'accessKey' => sys_config('accessKey'),
'secretKey' => sys_config('secretKey'),
];
break;
case 4:// cos 腾讯云
$config = [
'accessKey' => sys_config('tengxun_accessKey'),
'secretKey' => sys_config('tengxun_secretKey'),
'appid' => sys_config('tengxun_appid'),
];
break;
case 5://京东云
$config = [
'accessKey' => sys_config('jd_accessKey'),
'secretKey' => sys_config('jd_secretKey'),
'storageRegion' => sys_config('jd_storage_region'),
];
break;
case 6://华为云
$config = [
'accessKey' => sys_config('hw_accessKey'),
'secretKey' => sys_config('hw_secretKey'),
];
break;
case 7://天翼云
$config = [
'accessKey' => sys_config('ty_accessKey'),
'secretKey' => sys_config('ty_secretKey'),
];
break;
}
$thumb = SystemConfigService::more(['thumb_big_height', 'thumb_big_width', 'thumb_mid_height', 'thumb_mid_width', 'thumb_small_height', 'thumb_small_width',]);
$water = SystemConfigService::more([
'image_watermark_status',
'watermark_type',
'watermark_image',
'watermark_opacity',
'watermark_position',
'watermark_rotate',
'watermark_text',
'watermark_text_angle',
'watermark_text_color',
'watermark_text_size',
'watermark_x',
'watermark_y']);
$config = array_merge($config, ['thumb' => $thumb], ['water' => $water]);
//除了本地存储其他都去获取配置信息
if (1 !== $type) {
/** @var SystemStorageServices $make */
$make = app()->make(SystemStorageServices::class);
$res = $make->getConfig($type);
$config['uploadUrl'] = $res['domain'];
$config['storageName'] = $res['name'];
if ($type != 5) $config['storageRegion'] = $res['region'];
}
return self::$upload['upload_' . $type] = new Upload($type, $config);
}
/**
* 生辰缩略图水印实例化
* @param string $filePath
* @param bool $is_remote_down
* @return Upload
*/
public static function getOssInit(string $filePath, bool $is_remote_down = false)
{
//本地
$uploadUrl = sys_config('site_url');
if ($uploadUrl && strpos($filePath, $uploadUrl) !== false) {
$filePath = explode($uploadUrl, $filePath)[1] ?? '';
return self::init(1)->setFilepath($filePath);
}
$fileArr = parse_url($filePath);
$fileHost = ($fileArr['scheme'] ?? '') . '://' . ($fileArr['host'] ?? '');
/** @var SystemStorageServices $storageServices */
$storageServices = app()->make(SystemStorageServices::class);
$storageArr = $storageServices->cacheTag()->remember('storage_list', function () use ($storageServices) {
return $storageServices->getStorageList([], ['domain','type']);
});
foreach ($storageArr as $item) {
if ($fileHost == $item['domain']) {
return self::init($item['type'])->setFilepath($filePath);
}
}
//远程图片 下载到本地处理
if ($is_remote_down) {
try {
/** @var DownloadImageService $down */
$down = app()->make(DownloadImageService::class);
$data = $down->path('thumb_water')->downloadImage($filePath);
$filePath = $data['path'] ?? '';
} catch (\Throwable $e) {
//下载失败 传入原地址
}
}
return self::init(1)->setFilepath($filePath);
}
}

View File

@@ -0,0 +1,124 @@
<?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;
use think\facade\Config;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Label\Label;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;
/**
* Class UtilService
* @package crmeb\services
*/
class UtilService
{
/**
* 获取小程序二维码是否生成
* @param $url
* @return array
*/
public static function remoteImage($url)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($curl);
$result = json_decode($result, true);
if (is_array($result)) return ['status' => false, 'msg' => $result['errcode'] . '---' . $result['errmsg']];
return ['status' => true];
}
/**
* 修改 https 和 http 移动到common
* @param $url $url 域名
* @param int $type 0 返回https 1 返回 http
* @return string
*/
public static function setHttpType($url, $type = 0)
{
$domainTop = substr($url, 0, 5);
if ($type) {
if ($domainTop == 'https') $url = 'http' . substr($url, 5, strlen($url));
} else {
if ($domainTop != 'https') $url = 'https:' . substr($url, 5, strlen($url));
}
return $url;
}
/**
* 获取二维码
* @param $url
* @param $name
* @return array|bool|string
*/
public static function getQRCodePath(string $url, string $name, string $savePath = 'wechat')
{
if (!strlen(trim($url)) || !strlen(trim($name))) return false;
try {
$uploadType = sys_config('upload_type');
// 没有选择默认使用本地上传
if (!$uploadType) $uploadType = 1;
$uploadType = (int)$uploadType;
$siteUrl = sys_config('site_url');
if (!$siteUrl) return '请前往后台设置->系统设置->网站域名 填写您的域名格式为:http://域名';
$info = [];
$outfiles = Config::get('qrcode.cache_dir') . '/' . $savePath;
$root_outfile = root_path('public/' . $outfiles);
if (!is_dir($root_outfile))
mkdir($root_outfile, 0777, true);
// Create QR code
$writer = new PngWriter();
$qrCode = QrCode::create($url)
->setEncoding(new Encoding('UTF-8'))
->setErrorCorrectionLevel(new ErrorCorrectionLevelLow())
->setSize(300)
->setMargin(10)
->setRoundBlockSizeMode(new RoundBlockSizeModeMargin())
->setForegroundColor(new Color(0, 0, 0))
->setBackgroundColor(new Color(255, 255, 255));
$writer->write($qrCode)->saveToFile($root_outfile . $name);
if ($uploadType === 1) {
$info["code"] = 200;
$info["name"] = $name;
$info["dir"] = '/' . $outfiles . '/' . $name;
$info["time"] = time();
$info['size'] = 0;
$info['type'] = 'image/png';
$info["image_type"] = 1;
$info['thumb_path'] = '/' . $outfiles . '/' . $name;
return $info;
} else {
$upload = UploadService::init($uploadType);
$content = file_get_contents($root_outfile . $name);
$res = $upload->to($outfiles)->validate()->stream($content, $name);
if ($res === false) {
return $upload->getError();
}
$info = $upload->getUploadInfo();
$info['image_type'] = $uploadType;
return $info;
}
} catch (\Exception $e) {
return $e->getMessage();
}
}
}

View File

@@ -0,0 +1,92 @@
<?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;
use PullWord\PullWord;
/**
* 分词搜索类
* Class VicWordService
* @package crmeb\services
*/
class VicWordService
{
/**
* 请求地址
* @var string
*/
protected $url = 'https://sms.crmeb.net/api/v2/open/keyword';
// protected $url = 'http://test-api.crmeb.com/api/v2/open/keyword';
private static $instance = null;
public function __construct()
{
}
private function __clone()
{
}
public static function instance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getWord($str)
{
try {
$res = HttpService::getRequest($this->url ,['keyword' => $str]);
$data = json_decode($res, true) ?: [];
$data = array_column($data['data'] ?? [], 0);
} catch (\Throwable $e) {
$data = [];
}
//没有获取分词时,加上原始的词
if (!count($data)) {
$data[] = $str;
}
if ($data) {
foreach ($data as $key => &$item) {
$item = trim($item);
if (!$item) unset($data[$key]);
}
}
return array_merge($data);
}
public function getWordV1($str)
{
try {
$pullWord = new PullWord($str);
$result = $pullWord->pull()->toJson()->get();
$result = json_decode($result, true);
$data = is_array($result) ? array_column($result, 't') : [];
} catch (\Throwable $e) {
$data = [];
}
//没有获取分词时,加上原始的词
if (!count($data)) {
$data[] = $str;
}
return $data;
}
}

39
crmeb/services/ai/Ai.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace crmeb\services\ai;
use crmeb\basic\BaseManager;
use crmeb\services\AccessTokenServeService;
use think\Container;
use think\facade\Config;
class Ai extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\ai\\storage\\';
protected function getDefaultDriver()
{
return Config::get('ai.default', 'ai');
}
protected function invokeClass($class)
{
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
$this->getConfigFile();
if (!$this->config) {
$this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
}
$handleAccessToken = new AccessTokenServeService($this->config['access_key'] ?? '', $this->config['secret_key'] ?? '');
$handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile]);
$this->config = [];
return $handle;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace crmeb\services\ai\storage;
use crmeb\basic\BaseAi;
class Ai extends BaseAi
{
const CONVERSATION = 'v2/chat/conversation';
const CHAT = 'v2/chat/chat';
protected function initialize(array $config = [])
{
parent::initialize($config);
}
public function chat(string $systemContent, string $userContent)
{
$param['messages'] = [
[
'role' => 'system',
'content' => $systemContent
],
[
'role' => 'user',
'content' => $userContent
]
];
$param['max_tokens'] = '4096';
$param['temperature'] = '1';
$param['frequency_penalty'] = '0';
$param['presence_penalty'] = '0';
$param['model'] = 'deepseek-chat';
$param['response_format'] = '{ "type": "json_object" }';
$result = $this->accessToken->httpRequest(self::CHAT, $param);
return str_replace(['```json', '```'], '', $result['data']['choices'][0]['message']['content']);
}
}

View File

@@ -0,0 +1,40 @@
<?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\delivery;
use crmeb\basic\BaseManager;
use think\facade\Config;
/**
* Class Delivery
* @package crmeb\services\delivery
* @mixin \crmeb\services\delivery\storage\Dada
* @mixin \crmeb\services\delivery\storage\Uupt
*/
class Delivery extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\delivery\\storage\\';
/**
* 设置默认
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('delivery.default', 'dada');
}
}

View File

@@ -0,0 +1,380 @@
<?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\delivery\storage;
use crmeb\basic\BaseDelivery;
use think\exception\ValidateException;
class Dada extends BaseDelivery
{
const BASE_URL = 'https://newopen.imdada.cn';
const ADD_MERCHANT = '/merchantApi/merchant/add';
const ADD_SHOP = '/api/shop/add';
const UPDATE_SHOP = '/api/shop/update';
const GET_CITY_CODE = '/api/cityCode/list';
const GET_SHOP_DETAIL = '/api/shop/detail';
const GET_ORDER_PRICE = '/api/order/queryDeliverFee';
const ADD_ORDER_AFTER_QUERY = '/api/order/addAfterQuery';
const ADD_ORDER_STSATUS_QUERY = '/api/order/status/query';
const GET_REASONS = '/api/order/cancel/reasons';
const CANCEL_ORDER = '/api/order/formalCancel';
const GET_BALANCE = '/api/balance/query';
const GET_RECHARGE = '/api/recharge';
public $config;
protected array $status = [
1 => '待接单',
2 => '待取货',
3 => '配送中',
4 => '已完成',
5 => '已取消',
8 => '指派单',
9 => '妥投异常之物品返回中',
10 => '妥投异常之物品返回完成',
100 => '骑士到店',
1000 => '创建达达运单失败'
];
public function initialize(array $config)
{
$this->config = $config;
parent::initialize($config);
}
/**
* 创建商户
* @param $data
* @return mixed
*/
public function addMerchant($data)
{
return $this->sendRequest(self::ADD_MERCHANT, $data);
}
/**
* 创建门店
* @param $data
* @return mixed
*/
public function addShop($data)
{
$parmas[] = $data;
return $this->sendRequest(self::ADD_SHOP, $parmas);
}
/**
* 更新门店
* @param $data
* @return mixed
*/
public function updateShop($data)
{
$params['origin_shop_id'] = $data['origin_shop_id'];
if (isset($data['new_shop_id'])) $params['new_shop_id'] = $data['new_shop_id'];
if (isset($data['station_name'])) $params['station_name'] = $data['station_name'];
if (isset($data['business'])) $params['business'] = $data['business'];
if (isset($data['station_address'])) $params['station_address'] = $data['station_address'];
if (isset($data['lng'])) $params['lng'] = $data['lng'];
if (isset($data['lat'])) $params['lat'] = $data['lat'];
if (isset($data['contact_name'])) $params['contact_name'] = $data['contact_name'];
if (isset($data['phone'])) $params['phone'] = $data['phone'];
if (isset($data['status'])) $params['status'] = $data['status'];
return $this->sendRequest(self::UPDATE_SHOP, $params);
}
/**
* 预发布订单
* @param $data
* @return mixed
*/
public function addOrder($data)
{
$params = [
'deliveryNo' => $data['deliveryNo'],
];
return $this->sendRequest(self::ADD_ORDER_AFTER_QUERY, $params);
}
/**
* 计算订单价格
* @param $data
* @return mixed
*/
public function getOrderPrice($data)
{
$params = [
'shop_no' => $data['shop_no'],
'origin_id' => $data['origin_id'],
'city_code' => $data['city_code'],
'cargo_price' => $data['cargo_price'],
'is_prepay' => $data['is_prepay'],
'receiver_name' => $data['receiver_name'],
'receiver_address'=> $data['receiver_address'],
'callback' => $this->callback_url,
'cargo_weight' => $data['cargo_weight'],
'receiver_phone' => $data['receiver_phone'],
'is_finish_code_needed'=> $data['is_finish_code_needed'],
];
return $this->sendRequest(self::GET_ORDER_PRICE, $params);
}
/**
* 获取订单详情
* @param $data
* @return mixed
*/
public function getOrderDetail($data)
{
$params['order_id'] = $data['origin_id'];
$res = $this->sendRequest(self::ADD_ORDER_STSATUS_QUERY, $params);
if($res){
$res['status_name'] = $this->status[$res['statusCode']] ?? '';
$res['porter_name'] = $this->status[$res['transporterName']] ?? '';
$res['porter_phone'] = $this->status[$res['transporterPhone']] ?? '';
}
return $res;
}
/**
* 取消订单
* @param $data
* @return mixed
*/
public function cancelOrder($data)
{
$params['order_id'] = $data['origin_id'];
$params['cancel_reason'] = $data['cancel_reason'] ?? '无';
$params['cancel_reason_id'] = $data['reason'];
return $this->sendRequest(self::CANCEL_ORDER, $params);
}
/**
* 获取充值地址
* @param $data
* @return mixed
*/
public function getRecharge($data =[])
{
$params = [
'amount' => $data['amount'] ?? 100,
'category'=> $data['category'] ?? 'PC',
];
return $this->sendRequest(self::GET_RECHARGE, $params);
}
/**
* 获取余额
* @param $data
* @return mixed
*/
public function getBalance($data)
{
$params['category'] = $data['category'] ?? 3;
$res = $this->sendRequest(self::GET_BALANCE, $params);
return [
'deliverBalance' => $res['deliverBalance']
];
}
/**
* 支付小费
* @param $data
* @return mixed
*/
public function addTip($data)
{
return true;
}
/**
* 取消原因
* @param $data
* @return mixed
*/
public function reasons($data = '')
{
$options = $this->sendRequest(self::GET_REASONS, $data);
foreach ($options as $option) {
$value = $option['id'];
$label = $option['reason'];
$res[] = compact('value','label');
}
return $res;
}
/**
* 获取城市信息
* @param $data
* @return mixed
*/
public function getCity($data = '')
{
$res = $this->sendRequest(self::GET_CITY_CODE, $data);
foreach ($res as $item) {
$data[] = [
'key' => $item['cityName'],
'label' => $item['cityName'],
];
}
return $data;
}
/**
* 获取门店信息
* @param $data
* @return mixed
*/
public function getShopDetail($id)
{
$data = ['origin_shop_id' => $id];
return $this->sendRequest(self::GET_SHOP_DETAIL, $data);
}
public function getBusiness()
{
return [
['key' => 1 , 'label' => '食品小吃'],
['key' => 2 , 'label' => '饮料'],
['key' => 3 , 'label' => '鲜花绿植'],
['key' => 5 , 'label' => '其他'],
['key' => 8 , 'label' => '文印票务'],
['key' => 9 , 'label' => '便利店'],
['key' => 13 , 'label' => '水果生鲜'],
['key' => 19 , 'label' => '同城电商'],
['key' => 20 , 'label' => '医药'],
['key' => 21 , 'label' => '蛋糕'],
['key' => 24 , 'label' => '酒品'],
['key' => 25 , 'label' => '小商品市场'],
['key' => 26 , 'label' => '服装'],
['key' => 27 , 'label' => '汽修零配'],
['key' => 28 , 'label' => '数码家电'],
['key' => 29 , 'label' => '小龙虾/烧烤'],
['key' => 31 , 'label' => '超市'],
['key' => 51 , 'label' => '火锅'],
['key' => 53 , 'label' => '个护美妆'],
['key' => 55 , 'label' => '母婴'],
['key' => 57 , 'label' => '家居家纺'],
['key' => 59 , 'label' => '手机'],
['key' => 61 , 'label' => '家装'],
['key' => 63 , 'label' => '成人用品'],
];
}
public function sendRequest($api, $params)
{
$url = self::BASE_URL . $api;
$params = $this->bulidRequestParams($params);
$response = $this->httpRequestWithPost($url, $params);
$data = $this->getMessage($response);
return $data;
}
/**
* 构造请求数据
* data:业务参数,json字符串
*/
public function bulidRequestParams($params)
{
$requestParams = array();
$requestParams['app_key'] = $this->config['app_key'];
$requestParams['body'] = json_encode($params);
$requestParams['format'] = 'json';
$requestParams['v'] = '1.0';
$requestParams['source_id'] = $this->config['source_id'];
$requestParams['timestamp'] = time();
$requestParams['signature'] = $this->_sign($requestParams);
return json_encode($requestParams);
}
/**
* 签名生成signature
*/
public function _sign($data)
{
//1.升序排序
ksort($data);
//2.字符串拼接
$args = "";
foreach ($data as $key => $value) {
$args .= $key . $value;
}
$args = $this->config['app_secret'] . $args . $this->config['app_secret'];
//3.MD5签名,转为大写
$sign = strtoupper(md5($args));
return $sign;
}
/**
* 发送请求,POST
* @param $url 指定URL完整路径地址
* @param $data 请求的数据
*/
public function httpRequestWithPost($url, $data, $headers = [])
{
$headers = array(
'Content-Type: application/json',
);
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_TIMEOUT, 3);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$resp = curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
if (isset($info['http_code']) && $info['http_code'] == 200) {
return $resp;
}
return ;
}
protected function getMessage($json, $message = '未知错误!')
{
$data = json_decode($json, true);
//code=2551 订单创建中
if (!in_array($data['code'], [0, 2551])) {
isset($data['msg']) && $message = $data['msg'];
if ($data['errorCode'] == 7718) {
foreach ($data['result']['failedList'] as $datum) {
$message .= ':'.$datum['shopName'].'/'. $datum['msg'].';';
}
}
throw new ValidateException('【达达错误提示】:'.$message);
} else {
if ($data['status'] == 'success') return $data['result'] ?? $data;
return $data;
}
}
}

View File

@@ -0,0 +1,418 @@
<?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\delivery\storage;
use crmeb\basic\BaseDelivery;
use think\exception\ValidateException;
class Uupt extends BaseDelivery
{
//uu跑腿
public $config;
//域名
// const BASE_URL = 'http://openapi.test.uupt.com';
const BASE_URL = 'https://openapi.uupt.com';
//发布订单
const ADD_ORDER = '/v2_0/addorder.ashx';
//计算价格
const GET_ORDER_PRICE = '/v2_0/getorderprice.ashx';
//详情
const GET_ORDER_DETAIL = '/v2_0/getorderdetail.ashx';
//充值
const GET_RECHARGE = '/v2_0/getrecharge.ashx';
//取消
const CANCEL_ORDER = '/v2_0/cancelorder.ashx';
//查询门店
const GET_SHOP = '/v2_0/getshoplist.ashx';
//余额
const GET_BALANCEDE = '/v2_0/getbalancedetail.ashx';
//获取城市
const GET_CITY = '/v2_0/getcitylist.ashx';
protected array $status = [
1 => '下单成功',
3 => '跑男抢单',
4 => '已到达',
5 => '已取件',
6 => '到达目的地',
10 => '收件人已收货',
-1 => '订单取消'
];
public function initialize(array $config)
{
$this->config = $config;
parent::initialize($config);
}
/**
* 发布订单
* @param $data
* @return mixed
*/
public function addOrder($data)
{
$params = [
'price_token' => $data['price_token'],
'order_price' => $data['total_money'],
'balance_paymoney' => $data['need_paymoney'],
'receiver' => $data['receiver'],
'receiver_phone' => $data['receiver_phone'],
'callback_url' => $this->callback_url,
'push_type' => '0', //推送方式(0 开放订单,2测试订单)默认传0即可
'special_type' => '0',
'callme_withtake' => $data['callme_withtake'] ?? '0',
'pay_type' => 0,
];
if ($data['note']) $params['note'] = $data['note'];
return $this->sendRequest(self::ADD_ORDER, $params);
}
/**
* 计算订单价格
* @param $data
* @return mixed
*/
public function getOrderPrice($data)
{
$params = [
'from_address' => $data['from_address'],
'to_address' => $data['to_address'],
'city_name' => $data['city_name'],
'goods_type' => $data['goods_type'],
'send_type' => '0',
'to_lat' => (string)$data['to_lat'],
'to_lng' => (string)$data['to_lng'],
'from_lat' => (string)$data['from_lat'],
'from_lng' => (string)$data['from_lng'],
];
return $this->sendRequest(self::GET_ORDER_PRICE, $params);
}
/**
* 获取订单详情
* @param $data
* @return mixed
*/
public function getOrderDetail($data)
{
if ($data['order_sn']) $params['origin_id'] = $data['order_sn'];
if ($data['order_code']) $params['order_code'] = $data['order_code'];
$res = $this->sendRequest(self::GET_ORDER_DETAIL, $params);
if ($res) {
$res['status_name'] = $this->status[$res['state']] ?? '';
$res['porter_name'] = $this->status[$res['driver_name']] ?? '';
$res['porter_phone'] = $this->status[$res['driver_mobile']] ?? '';
}
return $res;
}
/**
* 取消订单
* @param $data
* @return mixed
*/
public function cancelOrder($data)
{
$params = [
'origin_id' => $data['origin_id'],
'order_code' => $data['order_code'],
'reason' => $data['reason'],
];
return $this->sendRequest(self::CANCEL_ORDER, $params);
}
/**
* 获取充值地址
* @param $data
* @return mixed
*/
public function getRecharge($data)
{
return $this->sendRequest(self::GET_RECHARGE, $data);
}
/**
* 获取余额
* @param $data
* @return mixed
*/
public function getBalance($data)
{
$res = $this->sendRequest(self::GET_BALANCEDE, $data);
return [
'deliverBalance' => $res['AccountMoney']
];
}
/**
* 支付小费
* @param $data
* @return mixed
*/
public function addTip($data)
{
return true;
}
/**
* 获取城市信息
* @param $data
* @return mixed
*/
public function getCity($data = [])
{
$res = $this->sendRequest(self::GET_CITY, $data);
foreach ($res['CityList'] as $item) {
$data[] = [
'key' => $item['CityName'],
'label' => $item['CityName'],
];
}
return $data;
}
/**
* 发送请求
* @param $api
* @param $params
* @return mixed
*/
public function sendRequest($api, $params = [])
{
$url = self::BASE_URL . $api;
$params = $this->bulidRequestParams($params);
$response = $this->httpRequestWithPost($url, $params);
$data = $this->getMessage($response);
return $data;
}
/**
* 构造请求数据
* @param array $params
* @return mixed
*/
public function bulidRequestParams(array $params = [])
{
$params['openid'] = $this->config['open_id'];
$params['appid'] = $this->config['app_id'];
$params['nonce_str'] = str_replace('-', '', $this->guid());
$params['timestamp'] = time();
$params['sign'] = $this->_sign($params);
$arr = [];
foreach ($params as $key => $value) {
$arr[] = $key . '=' . $value;
}
$curlPost = implode('&', $arr);
return $curlPost;
}
/**
* 生成guid
* @param $data
* @return mixed
*/
public function guid()
{
mt_srand((float)microtime() * 10000); //optional for php 4.2.0 and up.
$charid = strtoupper(md5(uniqid(rand(), true)));
$hyphen = chr(45); // "-"
$uuid = substr($charid, 0, 8) . $hyphen
. substr($charid, 8, 4) . $hyphen
. substr($charid, 12, 4) . $hyphen
. substr($charid, 16, 4) . $hyphen
. substr($charid, 20, 12);
return strtolower(str_replace('-', '', $uuid));
}
/**
* 签名生成sign
* @param $data
* @return mixed
*/
public function _sign($data)
{
ksort($data);
$str = '';
foreach ($data as $key => $value) {
if (!is_null($value)) {
$str .= $key . '=' . $value . '&';
}
}
$str .= 'key=' . $this->config['app_key'];
$str = mb_strtoupper($str, 'UTF-8');
// halt($data,$str);
return strtoupper(md5($str));
}
/**
* 发送请求,POST
* @param $url 指定URL完整路径地址
* @param $data 请求的数据
*/
public function httpRequestWithPost($url, $data)
{
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_TIMEOUT, 3);
$resp = curl_exec($curl);
$info = curl_getinfo($curl);
curl_close($curl);
return $resp;
}
/**
* 获取错误信息
* @param $json
* @param $message
* @return mixed
*/
protected function getMessage($json, $message = '未知错误!')
{
$data = json_decode($json, true);
if (!in_array($data['return_code'], ['ok', 'fail'])) {
isset($data['return_msg']) && $message = $data['return_msg'];
$mes = $message == '未知错误!' ? $this->getCodeMap($data['return_code']) : $message;
throw new ValidateException('【UU错误提示】:' . $mes);
} else {
return $data;
}
}
/**
* 获取错误代码
* @param string $key 代码
* @return String 错误代码与信息
*/
protected function getCodeMap($key)
{
$codeMap = [
'-101' => '参数格式校验错误',
'-102' => 'timestamp错误',
'-103' => 'appid无效',
'-104' => '签名校验失败',
'-105' => 'openid无效',
'-199' => '参数格式校验错误',
'-1001' => '无法解析起始地',
'-1002' => '无法解析目的地',
'-1003' => '无法获取订单城市相关信息',
'-1004' => '订单小类出现错误',
'-1005' => '没有用户信息',
'-1006' => '优惠券ID错误',
'-2001' => 'price_token无效',
'-2002' => 'price_token无效',
'-2003' => '收货人电话格式错误',
'-2004' => 'special_type错误',
'-2005' => 'callme_withtake错误',
'-2006' => 'order_price错误',
'-2007' => 'balance_paymoney错误',
'-2008' => '订单总金额错误',
'-2009' => '支付金额错误',
'-2010' => '用户不一致',
'-2011' => '手机号错误',
'-2012' => '不存在绑定关系',
'-4001' => '取消原因不能为空',
'-4002' => '订单编号无效',
'-5001' => '订单编号无效',
'-5002' => '订单编号无效',
'-5003' => '订单编号无效',
'-10001' => '发送频率过快,请稍候重试',
'-11001' => '请输入正确的验证码',
];
$info = isset($codeMap[$key]) ? $codeMap[$key] : false;
return $info;
}
/**
* 创建商户
* @param $data
* @return mixed
*/
public function addMerchant($data)
{
return true;
}
/**
* 获取门店信息
* @param $id
* @return array
*/
public function getShopDetail($id)
{
return [];
}
/**
* 创建门店
* @param $data
* @return mixed
*/
public function addShop($data)
{
return true;
}
/**
* 更新门店
* @param $data
* @return mixed
*/
public function updateShop($data)
{
return true;
}
/**
*
* @return mixed
*/
public function getBusiness()
{
return [
['key' => 1, 'label' => '美食'],
['key' => 2, 'label' => '鲜花'],
['key' => 3, 'label' => '蛋糕'],
['key' => 4, 'label' => '手机'],
['key' => 5, 'label' => '钥匙'],
['key' => 6, 'label' => '文件'],
['key' => 0, 'label' => '其他'],
];
}
}

View File

@@ -0,0 +1,293 @@
<?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\erp;
use crmeb\services\HttpService;
use crmeb\services\CacheService;
use think\facade\Config;
use think\facade\Log;
use think\helper\Str;
use Psr\SimpleCache\CacheInterface;
/**
* Class AccessTokenServeService
* @package crmeb\services
*/
class AccessToken extends HttpService
{
/**
* 配置
* @var string
*/
protected $account;
/**
* @var string
*/
protected $secret;
/**
* @var string
*/
protected $apiUrl;
/**
* 授权登录账号
* @var string
*/
protected $authAccount;
/**
* 授权登录密码
* @var string
*/
protected $authPassword;
/**
* @var CacheInterface
*/
protected $cache;
/**
* @var string
*/
protected $accessToken;
/**
* 驱动类型
* @var string
*/
protected $name;
/**
* 配置文件名
* @var string
*/
protected $configFile;
/**
* 缓存token
* @var string
*/
protected $cacheTokenPrefix = "_crmeb_erp";
/**
* 刷新token
* @var string
*/
protected $cacheRefreshTokenPrefix = "_crmeb_erp_re";
/**
* AccessTokenServeService constructor.
* AccessToken constructor.
* @param string $name
* @param string $configFile
* @param array $config
* @param CacheInterface|null $cache
*/
public function __construct(string $name, string $configFile, array $config, ?CacheInterface $cache = null)
{
if (!$cache) {
/** @var CacheService $cache */
$cache = app()->make(CacheService::class);
}
$this->account = isset($config["app_key"]) ? $config["app_key"] : config($configFile . '.stores.' . $name . '.app_key', '');
$this->secret = isset($config["secret"]) ? $config["secret"] : config($configFile . '.stores.' . $name . '.secret', '');
$this->authAccount = $config['login_account'] ?? config($configFile . '.stores.' . $name . '.login_account', '');
$this->authPassword = $config['login_password'] ?? config($configFile . '.stores.' . $name . '.login_password', '');
$this->cache = $cache;
$this->name = $name;
$this->configFile = $configFile;
$this->apiUrl = Config::get($configFile . '.stores.' . $name . '.url', '');
$this->apiUrl = 'https://openapi.jushuitan.com';
}
/**
* 获取配置
* @return array
*/
public function getConfig(): array
{
return [
'account' => $this->account,
'secret' => $this->secret
];
}
/**
* 获取请求链接
* @param string $url
* @return string
*/
public function getApiUrl(string $url = ''): string
{
return $url ? $this->apiUrl . $url : $this->apiUrl;
}
/**
* 获取appKey
* @return string
*/
public function getAccount(): string
{
return $this->account;
}
/**
* @return mixed|string
*/
public function getAuthAccount()
{
return $this->authAccount;
}
/**
* @return mixed|string
*/
public function getAuthPassword()
{
return $this->authPassword;
}
/**
* 获取appSecret
* @return string
*/
public function getSecret(): string
{
return $this->secret;
}
/**
* 获取token
* @return string
* @throws \Exception
*/
public function getAccessToken(): ?string
{
if (isset($this->accessToken)) {
return $this->accessToken;
}
/**
* @see getJushuitanAccessToken
*/
$action = 'get' . Str::studly($this->name) . 'AccessToken';
if (method_exists($this, $action)) {
return $this->{$action}();
} else {
throw new \RuntimeException(__CLASS__ . '->' . $action . '(),Method not worn in');
}
}
/**
* 获取聚水潭token
* @return mixed|null|string
* @throws \Exception
*/
protected function getJushuitanAccessToken(): ?string
{
//读缓存
$cacheKey = md5($this->account . '_' . $this->secret . $this->cacheTokenPrefix);
$this->accessToken = $this->cache->get($cacheKey);
//需要前端异步授权
if (empty($this->accessToken)) {
throw new \RuntimeException("请跳转授权", 610);
}
return $this->accessToken;
}
/**
* 设置AccessToken缓存
* @param string $accessToken
* @param int $expiresIn
* @return bool
*/
public function setAccessToken(string $accessToken, int $expiresIn): bool
{
if (empty($accessToken) || !is_numeric($expiresIn) || $expiresIn <= 0) {
return false;
}
//写缓存
$cacheKey = md5($this->account . '_' . $this->secret . $this->cacheTokenPrefix);
$this->cache->redisHandler()->tag('erp_shop')->set($cacheKey, $accessToken, $expiresIn);
$this->accessToken = $accessToken;
return true;
}
/**
* 设置AccessToken提前过期缓存
* @param string $accessToken
* @param int $expiresIn
* @return bool
*/
public function setTokenExpire(string $accessToken, int $expiresIn): bool
{
if (empty($accessToken) || !is_numeric($expiresIn) || $expiresIn <= 0) {
return false;
}
//写缓存
$cacheKey = md5($this->account . '_' . $this->secret . '_epr_expire');
$this->cache->redisHandler()->tag('erp_shop')->set($cacheKey, $accessToken, $expiresIn);
$this->accessToken = $accessToken;
return true;
}
/**
* 获取提前过期缓存
* @return bool|mixed|null
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function getTokenExpire()
{
$cacheKey = md5($this->account . '_' . $this->secret . '_epr_expire');
return $this->cache->get($cacheKey);
}
/**
* 设置refreshToken缓存
* @param string refreshToken
* @return bool
*/
public function setRefreshToken(string $refreshToken): bool
{
if (empty($refreshToken)) {
return false;
}
//写缓存
$cacheKey = md5($this->account . '_' . $this->secret . $this->cacheRefreshTokenPrefix);
$this->cache->redisHandler()->tag('erp_shop')->set($cacheKey, $refreshToken);
return true;
}
/**
* 获取refreshToken缓存
* @param string refreshToken
* @return string
*/
public function getRefreshToken(): string
{
//读缓存
$cacheKey = md5($this->account . '_' . $this->secret . $this->cacheRefreshTokenPrefix);
return $this->cache->get($cacheKey);
}
}

View File

@@ -0,0 +1,75 @@
<?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\erp;
use crmeb\basic\BaseManager;
use crmeb\services\erp\storage\Jushuitan;
use think\Container;
use think\facade\Config;
/**
* Class Erp
* @package crmeb\services\erp
* @mixin Jushuitan
*/
class Erp extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\erp\\storage\\';
protected $type = [
'nothing',
'jushuitan',
];
/**
* 默认驱动
* @return mixed
*/
protected function getDefaultDriver()
{
$this->config = [
'app_key' => sys_config('jst_appkey'),
'secret' => sys_config('jst_appsecret'),
'login_account' => sys_config('jst_login_account'),
'login_password' => sys_config('jst_login_password'),
];
return $this->type[sys_config('erp_type')];
}
/**
* 获取类的实例
* @param $class
* @return mixed|void
*/
protected function invokeClass($class)
{
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
$this->getConfigFile();
if (!$this->config) {
$this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
}
$handleAccessToken = new AccessToken($this->name, $this->configFile, $this->config);
$handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile]);
$this->config = [];
return $handle;
}
}

View File

@@ -0,0 +1,460 @@
<?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\erp\storage;
use crmeb\basic\BaseErp;
use crmeb\exceptions\AdminException;
use crmeb\exceptions\ErpException;
use crmeb\services\erp\storage\jushuitan\Comment;
use crmeb\services\erp\storage\jushuitan\Order;
use crmeb\services\erp\storage\jushuitan\Product;
use crmeb\services\erp\storage\jushuitan\Stock;
use EasyWeChat\Kernel\Support\Str;
use think\Collection;
use think\facade\Cache;
use think\Response;
/**
* Class Jushuitan
* @package crmeb\services\erp\storage
*/
class Jushuitan extends BaseErp
{
//==================商家授权==================
/**
* 获取授权参数
* @param string $state 透传数据
* @return array
*/
public function getAuthParams($state = ""): array
{
$params = [];
//开发者应用Key
$params["app_key"] = $this->accessToken->getAccount();
//当前请求的时间戳【单位是秒】
$params["timestamp"] = time();
//透传数据 非必填
$params["state"] = $state;
//交互数据的编码【utf-8】目前只能传utf-8,不能不传!
$params["charset"] = "utf-8";
//签名
$params["sign"] = $this->sign($params);
//授权跳转地址
$params["url"] = "https://openweb.jushuitan.com/auth";
return $params;
}
/**
* 获取AccessToken 用于验证授权回调是否成功
* @return string
* @throws \Exception
*/
public function getAccessToken(): string
{
return $this->accessToken->getAccessToken();
}
/**
* 设置AccessToken
* @param $at
* @return string
*/
public function setAccessToken($at): string
{
return $this->accessToken->setAccessToken($at, 999999);
}
/**
* 平台授权回调
* @return Response
*/
public function authCallback(): Response
{
$params = request()->get();
//验证必要参数 返回失败
if (!isset($params["app_key"]) || !isset($params["code"]) || !isset($params["sign"])) {
return response(["code" => 504]);
}
$appKey = $params["app_key"];
$code = $params["code"]; //授权码,有效期为15分钟
$sign = $params["sign"];
$state = isset($params["state"]) ? $params["state"] : ""; //透传数据
//appKey是否匹配 不匹配返回成功-抛弃消息
if ($appKey !== $this->accessToken->getAccount()) {
return response(["code" => 0, "msg" => "appKey不匹配"]);
}
//sign验证失败 返回失败
if ($sign !== $this->sign($params)) {
return response(["code" => 505, "msg" => "签名错误"]);
}
//code换access_token
$request = $this->code2accessToken($code);
//缓存token
$this->accessToken->setAccessToken($request["access_token"], $request["expires_in"]);
//token提前过期时间
$this->accessToken->setTokenExpire($request["access_token"], $request["expires_in"] - (48 * 60 * 60));
//缓存刷新token
$this->accessToken->setRefreshToken($request["refresh_token"]);
return response(["code" => 0]);
}
/**
* @return bool|mixed|null
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function getTokenExpire()
{
return $this->accessToken->getTokenExpire();
}
/**
* 授权临时code换access_token
* @param $code
* @return array
*/
public function code2accessToken($code): array
{
$url = $this->accessToken->getApiUrl("/openWeb/auth/accessToken");
//请求参数
$params = [];
//开发者应用Key
$params["app_key"] = $this->accessToken->getAccount();
//当前请求的时间戳【单位是秒】
$params["timestamp"] = time();
//固定值:authorization_code
$params["grant_type"] = "authorization_code";
//交互数据的编码【utf-8】目前只能传utf-8,不能不传!
$params["charset"] = "utf-8";
//授权码
$params["code"] = $code;
//签名
$params["sign"] = $this->sign($params);
try {
$request = $this->accessToken::postRequest($url, $params);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
//处理平台响应异常
$this->checkRequestError($request);
return $request["data"];
}
/**
* @param $content
* @return Collection
*/
protected function json($content)
{
if (false === $content) {
return collect();
}
$data = json_decode($content, true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new ErpException(sprintf('Failed to parse JSON: %s', json_last_error_msg()));
}
return collect($data);
}
/**
* 通过接口方式授权登录聚水潭
* @param string $account
* @param string $password
* @return bool
*/
public function authLogin(string $account = null, string $password = null)
{
if (Cache::has('erp_login_count') && Cache::get('erp_login_count') > 10) {
return false;
}
$authParams = $this->getAuthParams();
$loginInfo = $this->accessToken->postRequest('https://api.jushuitan.com/erp/webapi/UserApi/WebLogin/Passport', json_encode([
'ipAddress' => '',
'uid' => '',
'data' => [
'account' => $account ?: $this->accessToken->getAuthAccount(),
'j_d_3' => '',
'password' => $password ?: $this->accessToken->getAuthPassword(),
'v_d_144' => ''
],
]), ['Content-Type:application/json; charset=utf-8']);
$loginInfo = $this->json($loginInfo);
if ($loginInfo['code'] === null || $loginInfo['code'] !== 0) {
if ($loginInfo['code'] === 10001) {
$erpLoginCount = 0;
if (Cache::has('erp_login_count')) {
$erpLoginCount = Cache::get('erp_login_count', 0);
$erpLoginCount++;
}
Cache::set('erp_login_count', $erpLoginCount, 60);
}
throw new ErpException($loginInfo['msg'] ?? '登录失败');
}
$cookie = $loginInfo['cookie'];
$cookieData = [];
foreach ($cookie as $k => $v) {
$cookieData[] = $k . '=' . $v;
}
$res = $this->accessToken->postRequest('https://api.jushuitan.com/openWeb/auth/oauthAction', json_encode([
'uid' => '',
'data' => [
'app_key' => $authParams['app_key'],
'charset' => $authParams['charset'],
'sign' => $authParams['sign'],
'state' => '',
'timestamp' => $authParams['timestamp'],
],
]), [
'Content-Type:application/json',
'Cookie:' . implode(';', $cookieData),
]);
$res = $this->json($res);
if (!$res) {
throw new ErpException('请求失败');
}
if ($res['code'] != 0) {
throw new ErpException($res['msg'] ?? '授权失败');
}
return true;
}
/**
* 刷新access_token
* @return bool
*/
public function refreshToken(): bool
{
$refreshToken = $this->accessToken->getRefreshToken();
if (empty($refreshToken)) {
throw new AdminException("请跳转授权手动授权", 610);
}
$url = $this->accessToken->getApiUrl("/openWeb/auth/refreshToken");
//请求参数
$params = [];
//开发者应用Key
$params["app_key"] = $this->accessToken->getAccount();
//当前请求的时间戳【单位是秒】
$params["timestamp"] = time();
//固定值:refresh_token
$params["grant_type"] = "refresh_token";
//交互数据的编码【utf-8】目前只能传utf-8,不能不传!
$params["charset"] = "utf-8";
//更新令牌
$params["refresh_token"] = $refreshToken;
//固定值:all
$params["scope"] = "all";
//签名
$params["sign"] = $this->sign($params);
try {
$request = $this->accessToken::postRequest($url, $params);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
//处理平台响应异常
$this->checkRequestError($request);
//缓存token
$this->accessToken->setAccessToken($request["access_token"], $request["expires_in"]);
//token提前过期时间
$this->accessToken->setTokenExpire($request["access_token"], $request["expires_in"] - (48 * 60 * 60));
//缓存刷新token
$this->accessToken->setRefreshToken($request["refresh_token"]);
return true;
}
//==================内部方法==================
/**
* 发送post请求并处理异常
* @param $url
* @param $params
* @return mixed
*/
public function postRequest($url, $params)
{
//请求平台接口
$request = $this->accessToken->postRequest($url, $params);
$this->checkRequestError($request);
return $request;
}
/**
* 检测平台响应异常 并将响应转换为数组
* @param $request
*/
protected function checkRequestError(&$request)
{
if ($request === false || empty($request)) {
throw new AdminException('平台请求失败,请稍后重试');
}
$request = is_string($request) ? json_decode($request, true) : $request;
if (empty($request) || !isset($request['code'])) {
throw new AdminException('平台请求失败,请稍后重试!');
}
if (intval($request['code']) === 100) {
throw new AdminException("请重新授权", 610);
}
if ($request['code'] != 0) {
throw new AdminException(isset($request['msg']) ? '平台错误:' . $request['msg'] : '平台错误:发生异常,请稍后重试');
}
}
/**
* 拼装请求参数
* @param array $biz
* @return array
* @throws \Exception
* @throws \Exception
*/
public function getParams(array $biz = []): array
{
//请求参数
$params = [];
$accessToken = null;
try {
$accessToken = $this->accessToken->getAccessToken();
} catch (\Throwable $e) {
}
//刷新token
if (!$accessToken) {
$this->refreshToken();
$accessToken = $this->accessToken->getAccessToken();
}
if (!$accessToken) {
throw new ErpException('缺少access_token,请手动登录聚水潭开放平台进行授权登录');
}
//商户授权token值
$params["access_token"] = $accessToken;
//开发者应用Key
$params["app_key"] = $this->accessToken->getAccount();
//当前请求的时间戳【单位是秒】
$params["timestamp"] = time();
//接口版本,当前版本为【2】,目前只能传2,不能不传!
$params["version"] = "2";
//交互数据的编码【utf-8】目前只能传utf-8,不能不传!
$params["charset"] = "utf-8";
//业务请求参数,格式为jsonString
if (empty($biz)) {
$biz = new \ArrayObject();
}
$params["biz"] = json_encode($biz, JSON_UNESCAPED_UNICODE);
//签名
$params["sign"] = $this->sign($params);
return $params;
}
/**
* 计算签名
* @param array $params
* @return string
*/
protected function sign(array $params): string
{
if (empty($params)) {
return "";
}
//1.将请求参数中除 sign 外的多个键值对,根据键按照字典序排序
ksort($params);
//按照 "key1value1key2value2..." 的格式拼成一个字符串。
$str = "";
foreach ($params as $k => $v) {
if ($k == null || $k == "" || $k == "sign" || $v == "") {
continue;
}
if (is_array($v) || is_object($v)) {
$v = json_encode($v, JSON_UNESCAPED_UNICODE);
}
$str .= $k . $v;
}
//2.将 app_secret 拼接在 1 中排序后的字符串前面得到待签名字符串
$str = $this->accessToken->getSecret() . $str;
//3.使用 MD5 算法加密待加密字符串并转为小写
return bin2hex(md5($str, true));
}
/**
* @param string $type
* @return Stock|Order|Product|Comment
*/
public function serviceDriver(string $type = '')
{
$namespace = '\\crmeb\\services\\erp\\storage\\jushuitan\\';
$class = strpos($type, '\\') ? $type : $namespace . Str::studly($type);
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
return \think\Container::getInstance()->invokeClass($class, [$this->accessToken, $this]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace crmeb\services\erp\storage\jushuitan;
use crmeb\services\erp\AccessToken;
use crmeb\services\erp\storage\Jushuitan;
/**
* 公用接口
* Class Comment
* @package crmeb\services\erp\storage\jushuitan
*/
class Comment
{
/**
* token句柄
* @var AccessToken
*/
protected $accessToken;
/*** @var Jushuitan */
protected $jushuitan;
/**
* Comment constructor.
* @param AccessToken $accessToken
* @param Jushuitan $jushuitan
*/
public function __construct(AccessToken $accessToken, Jushuitan $jushuitan)
{
$this->accessToken = $accessToken;
$this->jushuitan = $jushuitan;
}
/**
* 获取商铺列表
* @param int $page
* @param int $limit
* @return mixed
* @throws \Exception
*/
public function getShopList(int $page = 1, int $limit = 10)
{
$api = $this->accessToken->getApiUrl('/open/shops/query');
$biz['items'] = ['page_index' => $page, 'page_size' => $limit];
$params = $this->jushuitan->getParams($biz);
return $this->jushuitan->postRequest($api, $params);
}
}

View File

@@ -0,0 +1,151 @@
<?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\erp\storage\jushuitan;
use crmeb\exceptions\AdminException;
use crmeb\services\erp\AccessToken;
use crmeb\services\erp\storage\Jushuitan;
use Exception;
class Order
{
/**
* token句柄
* @var AccessToken
*/
protected $accessToken;
/*** @var Jushuitan */
protected $jushuitan;
/**
* @param Jushuitan $jushuitan
*/
public function __construct(AccessToken $accessToken, Jushuitan $jushuitan)
{
$this->accessToken = $accessToken;
$this->jushuitan = $jushuitan;
}
/**
* 订单上传(推荐)
* @param array $data
* @return array
* @throws Exception
*/
public function ordersUpload(array $data): array
{
$url = $this->accessToken->getApiUrl("/open/jushuitan/orders/upload");
//拼装请求参数
$params = $this->jushuitan->getParams($data);
//请求平台接口
$request = $this->jushuitan->postRequest($url, $params);
return $request["data"];
}
/**
* 订单查询
* @param array $data
* @return array
* @throws Exception
*/
public function ordersSingleQuery(array $data): array
{
$url = $this->accessToken->getApiUrl("/open/orders/single/query");
//业务参数
$biz = [];
if (isset($data["page_index"])) {
//int 第几页,从第一页开始,默认1
$biz["page_index"] = intval($data["page_index"]);
}
if (isset($data["page_size"])) {
//int 每页多少条;默认30条,最大50条
$biz["page_size"] = intval($data["page_size"]);
}
if (isset($data["modified_begin"])) {
if (empty($data["modified_end"])) {
throw new AdminException("起始和结束时间必须同时存在");
}
//string日志起始时间,起始时间和 结束时间必须同时存在,时间间隔不能超过七天
$biz["modified_begin"] = strval($data["modified_begin"]);
$biz["modified_end"] = strval($data["modified_end"]);
}
///list 线上单号号,最大限制20条
if (isset($data["so_ids"])) {
$biz["so_ids"] = $data["so_ids"];
} elseif (empty($biz["modified_begin"]) || empty($biz["modified_end"])) {
//线上单号与修改时间不能同时为空
throw new AdminException("线上单号,与修改时间不能同时为空");
}
//店铺编号
if (!empty($data["shop_id"])) {
$biz["shop_id"] = $data["shop_id"];
}
//shop_id为0且is_offline_shop为true查询线下店铺单据 非必填 bool
if (isset($data["is_offline_shop"]) && !is_null($data["is_offline_shop"])) {
$biz["is_offline_shop"] = $data["is_offline_shop"];
}
if (!empty($data["status"])) {
$biz["status"] = $data["status"];
}
//拼装请求参数
$params = $this->jushuitan->getParams($biz);
//请求平台接口
$request = $this->jushuitan->postRequest($url, $params);
return $request["data"];
}
/**
* 订单取消-按内部单号取消
* @param array $data
* @return bool
* @throws Exception
*/
public function orderByOIdCancel(array $data): bool
{
$url = $this->accessToken->getApiUrl("/open/jushuitan/orderbyoid/cancel");
//拼装请求参数
$params = $this->jushuitan->getParams($data);
//请求平台接口
$this->jushuitan->postRequest($url, $params);
return true;
}
/**
* 实际收货上传
* @param array $list
* @return array
* @throws Exception
*/
public function afterSaleUpload(array $list): array
{
$url = $this->accessToken->getApiUrl("/open/aftersale/upload");
//拼装请求参数
$params = $this->jushuitan->getParams($list);
//请求平台接口
$request = $this->jushuitan->postRequest($url, $params);
return $request["data"];
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace crmeb\services\erp\storage\jushuitan;
use crmeb\services\erp\AccessToken;
use crmeb\services\erp\storage\Jushuitan;
class Product extends Jushuitan
{
/**
* token句柄
* @var AccessToken
*/
protected $accessToken;
/*** @var Jushuitan */
protected $jushuitan;
/**
* @param AccessToken $accessToken
* @param Jushuitan $jushuitan
*/
public function __construct(AccessToken $accessToken, Jushuitan $jushuitan)
{
$this->accessToken = $accessToken;
$this->jushuitan = $jushuitan;
}
/**
* 上传商品
* @param $data
* @return mixed
* @throws \Exception
*/
public function updateProduct($data)
{
$url = $this->accessToken->getApiUrl("/open/jushuitan/itemsku/upload");
//业务参数
$biz['items'] = $data;
//拼装请求参数
$params = $this->getParams($biz);
//请求平台接口
$request = $this->postRequest($url, $params);
if ($request['code'] == 0) {
return true;
} else {
return false;
}
}
/**
* 上传店铺商品
* @param $data
* @return mixed
* @throws \Exception
*/
public function updateShopProduct($data)
{
$url = $this->accessToken->getApiUrl("/open/jushuitan/skumap/upload");
//业务参数
$biz['items'] = $data;
//拼装请求参数
$params = $this->getParams($biz);
//请求平台接口
$request = $this->postRequest($url, $params);
if ($request['code'] == 0) {
return true;
} else {
return false;
}
}
/**
* 同步商品
* @param $spuArr
* @return mixed
* @throws \Exception
*/
public function syncProduct($spuArr)
{
$url = $this->accessToken->getApiUrl("/open/mall/item/query");
//业务参数
$biz['i_ids'] = $spuArr;
//拼装请求参数
$params = $this->getParams($biz);
//请求平台接口
$request = $this->postRequest($url, $params);
//获取ERP商品信息
return $request["data"];
}
/**
* 库存查询
* @param string $codeStr
* @return mixed
* @throws \Exception
*/
public function syncStock(string $codeStr)
{
$url = $this->accessToken->getApiUrl("/open/inventory/query");
//业务参数
$biz = [];
$biz["sku_ids"] = $codeStr;
//拼装请求参数
$params = $this->getParams($biz);
//请求平台接口
$request = $this->postRequest($url, $params);
return $request["data"];
}
}

View File

@@ -0,0 +1,151 @@
<?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\erp\storage\jushuitan;
use app\services\product\product\StoreProductServices;
use app\services\product\sku\StoreProductAttrValueServices;
use crmeb\exceptions\AdminException;
use crmeb\services\erp\AccessToken;
use crmeb\services\erp\storage\Jushuitan;
class Stock
{
/**
* token句柄
* @var AccessToken
*/
protected $accessToken;
/*** @var Jushuitan */
protected $jushuitan;
/**
* @param AccessToken $accessToken
* @param Jushuitan $jushuitan
*/
public function __construct(AccessToken $accessToken, Jushuitan $jushuitan)
{
$this->accessToken = $accessToken;
$this->jushuitan = $jushuitan;
}
/**
* 同步商品库存
* @param array $ids
* @return void
* @throws \Exception
*/
public function syncStock(string $ids = '')
{
/** @var StoreProductServices $storeProductServices */
$storeProductServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrValueServices $storeProductAttrValueServices */
$storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class);
//查询ids下的所有规格对应的sku
$ids = array_unique(array_map('intval', explode(',', $ids)));
$list = $storeProductAttrValueServices->getSkuArray(['product_id' => $ids, 'type' => 0], 'code', 'id');
$values = array_filter(array_values($list));
if (empty($values)) {
throw new AdminException('没有符合同步库存的商品');
}
$skuData = $skuMap = [];
$basic = 20; // 单次查询数量最多20
$num = count($values);
$rate = ceil($num / $basic);
for ($i = 0; $i < $rate; $i++) {
$code = array_slice($values, $i * $basic, $basic);
$skuMap = $skuMap + $this->getSkuStockByCode($code);
}
// 拼装规格数据
if (!empty($skuMap)) {
foreach ($skuMap as $key => $item) {
if ($id = array_search($key, $list)) {
$skuData[] = ['id' => $id, 'stock' => $item, 'sum_stock' => $item];
}
}
}
// 同步库存 TODO:待添加至队列
$storeProductServices->transaction(function () use ($ids, $skuData, $skuMap, $storeProductAttrValueServices, $storeProductServices) {
// 同步规格库存
$storeProductAttrValueServices->saveAll($skuData);
// 同步商品库存
$productData = $storeProductAttrValueServices->getProductStockByValues($ids);
$storeProductServices->saveAll($productData);
//同步门店库存
foreach ($skuMap as $item) {
}
});
return true;
}
/**
* 库存查询
* @param array $data
* @return array
* @throws \Exception
*/
public function inventoryQuery(string $codeStr): array
{
$url = $this->accessToken->getApiUrl("/open/inventory/query");
//业务参数
$biz = [];
$biz["sku_ids"] = $codeStr;
//拼装请求参数
$params = $this->getParams($biz);
//请求平台接口
$request = $this->postRequest($url, $params);
return $request["data"];
}
/**
* 获取erp库存
* @param array $code
* @return array
* @throws \Exception
*/
public function getSkuStockByCode(array $code = []): array
{
$skuMap = [];
$codeStr = implode(',', $code);
$result = $this->inventoryQuery($codeStr);
if (!empty($result['inventorys'])) {
foreach ($result['inventorys'] as $inventory) {
$skuMap[$inventory['sku_id']] = $inventory['qty'];
}
}
return $skuMap;
}
/**
* @param $name
* @param $arguments
* @return mixed
*/
public function __call($name, $arguments)
{
return call_user_func_array([$this->jushuitan, $name], $arguments);
}
}

View File

@@ -0,0 +1,62 @@
<?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\express;
use crmeb\basic\BaseManager;
use crmeb\services\AccessTokenServeService;
use think\Container;
use think\facade\Config;
/**
* Class Express
* @package crmeb\services\express
* @mixin \crmeb\services\express\storage\Express
*/
class Express extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\express\\storage\\';
/**
* 默认驱动
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('express.default', 'express');
}
/**
* 获取类的实例
* @param $class
* @return mixed|void
*/
protected function invokeClass($class)
{
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
$this->getConfigFile();
if (!$this->config) {
$this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
}
$handleAccessToken = new AccessTokenServeService($this->config['access_key'] ?? '', $this->config['secret_key'] ?? '', app()->cache);
$handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile]);
$this->config = [];
return $handle;
}
}

View File

@@ -0,0 +1,300 @@
<?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\express\storage;
use app\services\other\ExpressServices;
use crmeb\basic\BaseExpress;
use crmeb\exceptions\ApiException;
use crmeb\services\AccessTokenServeService;
/**
* Class Express
* @package crmeb\services\express\storage
*/
class Express extends BaseExpress
{
//注册服务
const EXPRESS_OPEN = 'v2/expr/open';
//电子面单模版
const EXPRESS_TEMP = 'v2/expr_dump/temp';
//快递公司
const EXPRESS_LIST = 'v2/expr/express';
//快递查询
const EXPRESS_QUERY = 'v2/expr/query';
//面单打印
const EXPRESS_DUMP = 'v2/expr/dump';
//获取物流公司信息
const SHIPMENT_KUAIDI_NUMS = 'v2/shipment/get_kuaidi_coms';
//创建商家寄件订单
const SHIPMENT_CREATE_ORDER = 'v2/shipment/create_order';
//取消商家寄件
const SHIPMENT_CANCEL_ORDER = 'v2/shipment/cancel_order';
//获取商家寄件订单列表
const SHIPMENT_INDEX = 'v2/shipment/index';
//获取商家寄件订单预扣金额
const SHIPMENT_PRICE = 'v2/shipment/price';
/** 初始化
* @param array $config
* @return mixed|void
*/
protected function initialize(array $config = [])
{
parent::initialize($config);
}
/**
* 商家寄件获取快递公司
* @return array|mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/15
*/
public function getKuaidiComs()
{
$list = $this->accessToken->httpRequest(self::SHIPMENT_KUAIDI_NUMS, [], 'GET');
foreach ($list as &$item) {
$item['code'] = $item['value'];
$item['value'] = $item['label'];
$num = 1;
foreach ($item['list'] as &$value) {
$value['title'] = $item['label'] . '模版' . $num;
$num++;
}
}
return $list;
}
/**
* 商家寄件创建订单
* @param array $data
* @return array|mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/15
*/
public function shippmentCreateOrder(array $data)
{
$siid = sys_config('config_export_siid');
$param = [
'kuaidicom' => $data['kuaidicom'],
'man_name' => $data['man_name'],
'phone' => $data['phone'],
'address' => $data['address'],
'send_real_name' => $data['send_real_name'],
'send_phone' => $data['send_phone'],
'send_address' => $data['send_address'],
'call_back_url' => sys_config('site_url') . '/api/order_call_back',
'return_type' => $siid ? '10' : '20',
'siid' => $siid,
'tempid' => $data['temp_id'],
'cargo' => $data['cargo'],
'weight' => $data['weight'],
'day_type' => $data['day_type'],
'pickup_start_time' => $data['pickup_start_time'],
'pickup_end_time' => $data['pickup_end_time'],
];
return $this->accessToken->httpRequest(self::SHIPMENT_CREATE_ORDER, $param, 'post');
}
/**
* 取消商家寄件订单
* @param array $data
* @return array|mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/15
*/
public function shipmentCancelOrder(array $data)
{
$param = [
'task_id' => $data['task_id'],//快递100商家寄件任务id
'order_id' => $data['order_id'],//快递100商家寄件发起的订单号。并不是系统中的订单号
'cancel_msg' => $data['cancel_msg'],//取消原因
];
return $this->accessToken->httpRequest(self::SHIPMENT_CANCEL_ORDER, $param);
}
/**
* 获取商家寄件订单列表
* @param array $data
* @return array|mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/15
*/
public function getShipmentOrderList(array $data)
{
$param = [
'kuaidi_num' => $data['kuaidi_num'] ?? '',
'courier_name' => $data['courier_name'] ?? '',
'page' => $data['page'] ?? 1,
'limit' => $data['limit'] ?? 10,
];
return $this->accessToken->httpRequest(self::SHIPMENT_INDEX, $param, 'GET');
}
/**
* @param array $data
* @return array|mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/6/16
*/
public function getPrice(array $data)
{
if (empty($data['kuaidicom'])) {
throw new ApiException('快递编码必须填写');
}
if (empty($data['send_address'])) {
throw new ApiException('寄件地址必须填写');
}
if (empty($data['address'])) {
throw new ApiException('收件地址必须填写');
}
$param = [
'kuaidicom' => $data['kuaidicom'],
'send_address' => $data['send_address'],
'address' => $data['address'] ?? '',
'weight' => $data['weight'] ?? '',
'service_type' => $data['service_type'] ?? '',
];
return $this->accessToken->httpRequest(self::SHIPMENT_PRICE, $param);
}
/**
* 开通物流服务
* @return bool|mixed
*/
public function open()
{
return $this->accessToken->httpRequest(self::EXPRESS_OPEN, []);
}
/**
* 获取电子面单模版
* @param $com 快递公司编号
* @param int $page
* @param int $limit
* @return bool|mixed
*/
public function temp(string $com)
{
$param = [
'com' => $com
];
$header = [];
if (!sys_config('config_export_siid')) {
$header = ['version:v1.1'];
}
return $this->accessToken->httpRequest(self::EXPRESS_TEMP, $param, 'GET', true, $header);
}
/**
* 获取物流公司列表
* @param int $type 快递类型:1,国内运输商;2,国际运输商;3,国际邮政
* @return bool|mixed
*/
public function express(int $type = 0, int $page = 0, int $limit = 20)
{
if ($type) {
$param = [
'type' => $type,
'page' => $page,
'limit' => $limit
];
} else {
$param = [];
}
return $this->accessToken->httpRequest(self::EXPRESS_LIST, $param,'GET');
}
/**
* 查询物流信息
* @param $com
* @param $num
* @return bool|mixed
* @return 是否签收 ischeck
* @return 物流状态:status 0在途,1揽收,2疑难,3签收,4退签,5派件,6退回,7转单,10待清关,11清关中,12已清关,13清关异常,14收件人拒签
* @return 物流详情 content
*/
public function query(string $num, string $com = '')
{
$param = [
'com' => $com,
'num' => $num
];
if ($com === null) {
unset($param['com']);
}
return $this->accessToken->httpRequest(self::EXPRESS_QUERY, $param);
}
/**
* 电子面单打印
* @param array $data 必需参数: com(快递公司编码)、to_name(寄件人)、to_tel(寄件人电话)、to_addr(寄件人详细地址)、from_name(收件人)、from_tel(收件人电话)、from_addr(收件人地址)、temp_id(电子面单模板ID)、siid(云打印机编号)、count(商品数量)
* @return bool|mixed
*/
public function dump($data)
{
$param = $data;
$param['com'] = $data['com'] ?? '';
if (!$param['com']) throw new ApiException('快递公司编码缺失');
$param['to_name'] = $data['to_name'] ?? '';
$param['to_tel'] = $data['to_tel'] ?? '';
// $param['order_id'] = $data['order_id'] ?? '';
$param['to_addr'] = $data['to_addr'] ?? '';
if (!$param['to_addr'] || !$param['to_tel'] || !$param['to_name']) throw new ApiException('寄件人信息缺失');
$param['from_name'] = $data['from_name'] ?? '';
$param['from_tel'] = $data['from_tel'] ?? '';
$param['from_addr'] = $data['from_addr'] ?? '';
if (!$param['from_name'] || !$param['from_tel'] || !$param['from_addr']) throw new ApiException('收件人信息缺失');
$param['temp_id'] = $data['temp_id'] ?? '';
if (!$param['temp_id']) {
throw new ApiException('电子面单模板ID缺失');
}
$param['siid'] = sys_config('config_export_siid');
// if (!$param['siid']) {
// throw new ApiException('云打印机编号缺失');
// }
$param['count'] = $data['count'] ?? '';
$param['cargo'] = $data['cargo'] ?? '';
$param['customParam'] = $data['customParam'] ?? '';
if (!$param['count']) {
throw new ApiException('商品数量缺失');
}
/** @var ExpressServices $expressServices */
$expressServices = app()->make(ExpressServices::class);
$expressData = $expressServices->getOneByWhere(['code' => $param['com']])->toArray();
if (isset($data['cargo'])) $param['cargo'] = $data['cargo'];
if ($expressData['partner_id'] == 1) $param['partner_id'] = $expressData['account'];
if ($expressData['partner_key'] == 1) $param['partner_key'] = $expressData['key'];
if ($expressData['net'] == 1) $param['net'] = $expressData['net_name'];
if ($expressData['check_man'] == 1) $param['checkMan'] = $expressData['courier_name'];
if ($expressData['partner_name'] == 1) $param['partnerName'] = $expressData['customer_name'];
if ($expressData['is_code'] == 1) $param['code'] = $expressData['code_name'];
//修改没有打印机的时候print_type=IMAGE,就会返回面单图片
$header = [];
if (!$data['siid']) {
$param['print_type'] = 'IMAGE';
$header = ['version:v1.1'];
}
return $this->accessToken->httpRequest(self::EXPRESS_DUMP, $param, 'POST', true, $header);
}
}

View File

@@ -0,0 +1,188 @@
<?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\printer;
use app\services\other\CacheServices;
use crmeb\services\HttpService;
use think\facade\Config;
use think\helper\Str;
/**
*
* Class AccessToken
* @package crmeb\services\printer
*/
class AccessToken extends HttpService
{
/**
* token
* @var array
*/
protected $accessToken;
/**
* 请求接口
* @var string
*/
protected $apiUrl;
/**
* @var string
*/
protected $clientId;
/**
* 终端号码
* @var string
*/
protected $machineCode;
/**
* 开发者id
* @var string
*/
protected $partner;
/**
* 驱动类型
* @var string
*/
protected $name;
/**
* 配置文件名
* @var string
*/
protected $configFile;
/**
* api key
* @var string
*/
protected $apiKey;
/**
* 飞鹅云SN
* @var string
*/
protected $feySn;
/**
* 飞鹅云UYEK
* @var string
*/
protected $feyUkey;
/**
* 飞鹅云USER
* @var string
*/
protected $feyUser;
public function __construct(array $config = [], string $name = '', string $configFile = '')
{
$this->clientId = $config['clientId'] ?? null;
$this->apiKey = $config['apiKey'] ?? null;
$this->partner = $config['partner'] ?? null;
$this->machineCode = $config['terminal'] ?? null;
$this->feyUser = $config['feyUser'] ?? null;
$this->feyUkey = $config['feyUkey'] ?? null;
$this->feySn = $config['feySn'] ?? null;
$this->name = $name;
$this->configFile = $configFile;
$this->apiUrl = Config::get($this->configFile . '.stores.' . $this->name . '.apiUrl', 'https://open-api.10ss.net/');
}
/**
* 获取token
* @return mixed|null|string
* @throws \Exception
*/
public function getAccessToken()
{
if (isset($this->accessToken[$this->name])) {
return $this->accessToken[$this->name];
}
$action = 'get' . Str::studly($this->name) . 'AccessToken';
if (method_exists($this, $action)) {
return $this->{$action}();
} else {
throw new \RuntimeException(__CLASS__ . '->' . $action . '(),Method not worn in');
}
}
/**
* 获取易联云token
* @return mixed|null|string
* @throws \Exception
*/
protected function getYiLianYunAccessToken()
{
/** @var CacheServices $cacheServices */
$cacheServices = app()->make(CacheServices::class);
$this->accessToken[$this->name] = $cacheServices->getDbCache('YLY_access_token', function () {
$request = self::postRequest($this->apiUrl . 'oauth/oauth', [
'client_id' => $this->clientId,
'grant_type' => 'client_credentials',
'sign' => strtolower(md5($this->clientId . time() . $this->apiKey)),
'scope' => 'all',
'timestamp' => time(),
'id' => $this->createUuid(),
]);
$request = json_decode($request, true);
$request['error'] = $request['error'] ?? 0;
$request['error_description'] = $request['error_description'] ?? '';
if ($request['error'] == 0 && $request['error_description'] == 'success') {
return $request['body']['access_token'] ?? '';
}
return '';
},86400);
if (!$this->accessToken[$this->name])
throw new \Exception('获取access_token获取失败');
return $this->accessToken[$this->name];
}
/**
* 获取请求链接
* @return string
*/
public function getApiUrl(string $url = '')
{
return $url ? $this->apiUrl . $url : $this->apiUrl;
}
/**
* 生成UUID4
* @return string
*/
public function createUuid()
{
mt_srand();
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
}
/**
* 获取属性
* @param $name
* @return mixed
*/
public function __get($name)
{
if (in_array($name, ['clientId', 'apiKey', 'accessToken', 'partner', 'terminal', 'machineCode', 'feyUser', 'feyUkey', 'feySn'])) {
return $this->{$name};
}
}
}

View File

@@ -0,0 +1,72 @@
<?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\printer;
use crmeb\basic\BaseManager;
use think\facade\Config;
use think\Container;
/**
* Class Printer
* @package crmeb\services\auth
* @mixin \crmeb\services\printer\storage\YiLianYun
* @mixin \crmeb\services\printer\storage\FeiEYun
*/
class Printer extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\printer\\storage\\';
/**
* @var object
*/
protected $handleAccessToken;
/**
* 默认驱动
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('printer.default', 'yi_lian_yun');
}
/**
* 获取类的实例
* @param $class
* @return mixed|void
*/
protected function invokeClass($class)
{
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
$this->getConfigFile();
if (!$this->config) {
$this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
}
if (!$this->handleAccessToken) {
$this->handleAccessToken = new AccessToken($this->config, $this->name, $this->configFile);
}
$handle = Container::getInstance()->invokeClass($class, [$this->name, $this->handleAccessToken, $this->configFile]);
$this->config = [];
return $handle;
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace crmeb\services\printer\storage;
use app\services\activity\table\TableQrcodeServices;
use crmeb\basic\BasePrinter;
class FeiEYun extends BasePrinter
{
protected int $times;
/**
* 初始化
* @param array $config
* @return mixed|void
*/
protected function initialize(array $config)
{
}
/**
* 开始打印
* @return bool|mixed|string
* @throws \Exception
*/
public function startPrinter()
{
if (!$this->printerContent) {
return $this->setError('Missing print');
}
$time = time();
$request = $this->accessToken->postRequest('http://api.feieyun.cn/Api/Open/', [
'user' => $this->accessToken->feyUser,
'stime' => $time,
'sig' => sha1($this->accessToken->feyUser . $this->accessToken->feyUkey . $time),
'apiname' => 'Open_printMsg',
'sn' => $this->accessToken->feySn,
'content' => $this->printerContent,
'times' => $this->times
]);
$res = json_decode($request, true);
if ($res['msg'] == 'ok') {
return $res;
} else {
return $this->setError($res['msg']);
}
}
/**
* 设置打印内容
* @param $content
* @param int $times
* @return FeiEYun
*/
public function setPrinterContent($content, $times = 1): self
{
$this->times = $times;
$this->printerContent = $content;
return $this;
}
/**
* 设置桌码打印内容
* @param array $config
* @return YiLianYun
*/
public function setPrinterTableContent(array $config): self
{
$timeYmd = date('Y-m-d', time());
$timeHis = date('H:i:s', time());
$product = $config['product'];
$tableInfo = $config['tableInfo'];
$name = $config['name'];
/** @var TableQrcodeServices $qrcodeService */
$qrcodeService = app()->make(TableQrcodeServices::class);
$Info = $qrcodeService->getQrcodeyInfo((int)$tableInfo['qrcode_id'], ['cateName']);
$this->printerContent = '<CB>**' . $config['name'] . '**</CB><BR>';
$this->printerContent .= '--------------------------------<BR>';
$this->printerContent .= '桌码流水:' . $tableInfo['serial_number'] . '<BR>';
$this->printerContent .= '桌码分类: ' . $Info['cateName']['name'] . '<BR>';
$this->printerContent .= '桌码编号: ' . $Info['table_number'] . '<BR>';
$this->printerContent .= '日 期: ' . $timeYmd . '<BR>';
$this->printerContent .= '时 间:' . $timeHis . '<BR>';
$this->printerContent .= '**************商品**************<BR>';
$this->printerContent .= '名称 单价 数量 金额<BR>';
$this->printerContent .= '--------------------------------<BR>';
foreach ($product as $item) {
$name = $item['productInfo']['store_name'] . " | " . $item['productInfo']['attrInfo']['suk'];
$price = $item['truePrice'];
$num = $item['cart_num'];
$prices = bcmul((string)$item['cart_num'], (string)$item['truePrice'], 2);
$kw3 = '';
$kw1 = '';
$kw2 = '';
$kw4 = '';
$str = $name;
$blankNum = 14;//名称控制为14个字节
$lan = mb_strlen($str, 'utf-8');
$m = 0;
$j = 1;
$blankNum++;
$result = array();
if (strlen($price) < 6) {
$k1 = 6 - strlen($price);
for ($q = 0; $q < $k1; $q++) {
$kw1 .= ' ';
}
$price = $price . $kw1;
}
if (strlen($num) < 3) {
$k2 = 3 - strlen($num);
for ($q = 0; $q < $k2; $q++) {
$kw2 .= ' ';
}
$num = $num . $kw2;
}
if (strlen($prices) < 6) {
$k3 = 6 - strlen($prices);
for ($q = 0; $q < $k3; $q++) {
$kw4 .= ' ';
}
$prices = $prices . $kw4;
}
for ($i = 0; $i < $lan; $i++) {
$new = mb_substr($str, $m, $j, 'utf-8');
$j++;
if (mb_strwidth($new, 'utf-8') < $blankNum) {
if ($m + $j > $lan) {
$m = $m + $j;
$tail = $new;
$lenght = iconv("UTF-8", "GBK//IGNORE", $new);
$k = 14 - strlen($lenght);
for ($q = 0; $q < $k; $q++) {
$kw3 .= ' ';
}
if ($m == $j) {
$tail .= $kw3 . ' ' . $price . ' ' . $num . ' ' . $prices;
} else {
$tail .= $kw3 . '<BR>';
}
break;
} else {
$next_new = mb_substr($str, $m, $j, 'utf-8');
if (mb_strwidth($next_new, 'utf-8') < $blankNum) {
continue;
} else {
$m = $i + 1;
$result[] = $new;
$j = 1;
}
}
}
}
$head = '';
foreach ($result as $key => $value) {
if ($key < 1) {
$v_lenght = iconv("UTF-8", "GBK//IGNORE", $value);
$v_lenght = strlen($v_lenght);
if ($v_lenght == 13) $value = $value . " ";
$head .= $value . ' ' . $price . ' ' . $num . ' ' . $prices;
} else {
$head .= $value . '<BR>';
}
}
$this->printerContent .= $head . $tail;
unset($price);
}
$this->printerContent .= '--------------------------------<BR>';
$this->printerContent .= '商品金额:' . number_format(array_sum(array_column($product, 'price')), 1) . '元<BR>';
return $this;
}
}

View File

@@ -0,0 +1,89 @@
<?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\printer\storage;
use crmeb\basic\BasePrinter;
use crmeb\services\printer\AccessToken;
/**
* Class YiLianYun
* @package crmeb\services\printer\storage
*/
class YiLianYun extends BasePrinter
{
/**
* 初始化
* @param array $config
* @return mixed|void
*/
protected function initialize(array $config)
{
}
/**
* 开始打印
* @return bool|mixed|string
* @throws \Exception
*/
public function startPrinter()
{
if (!$this->printerContent) {
return $this->setError('Missing print');
}
$time = time();
$requestData = [
'client_id' => $this->accessToken->clientId,
'access_token' => $this->accessToken->getAccessToken(),
'machine_code' => $this->accessToken->machineCode,
'content' => $this->printerContent,
'origin_id' => 'crmeb' . $time,
'sign' => strtolower(md5($this->accessToken->clientId . $time . $this->accessToken->apiKey)),
'id' => $this->accessToken->createUuid(),
'timestamp' => $time
];
try {
$request = $this->accessToken->postRequest($this->accessToken->getApiUrl('print/index'), $requestData);
$request = is_string($request) ? json_decode($request, true) : $request;
if (isset($request['error']) && $request['error'] == '18') {//token过期
$cacheServices = app()->make(CacheServices::class);
$cacheServices->delectDbCache('YLY_access_token_'. $this->accessToken->clientId);
$requestData['access_token'] = $this->accessToken->getAccessToken();
$request = $this->accessToken->postRequest($this->accessToken->getApiUrl('print/index'), $requestData);
}
} catch (\Exception $e) {
return $this->setError($e->getMessage());
}
$this->printerContent = null;
if ($request === false) {
return $this->setError('request was aborted');
}
$request = is_string($request) ? json_decode($request, true) : $request;
if (isset($request['error']) && $request['error'] != 0) {
return $this->setError(isset($request['error_description']) ? $request['error_description'] : 'Accesstoken has expired');
}
return $request;
}
/**
* 设置打印内容
* @param $content
* @param int $times
* @return YiLianYun
*/
public function setPrinterContent($content, $times = 1): self
{
$this->printerContent = $content;
return $this;
}
}

View File

@@ -0,0 +1,64 @@
<?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\product;
use crmeb\basic\BaseManager;
use crmeb\services\AccessTokenServeService;
use think\facade\Config;
use think\Container;
/**
* Class Product
* @package crmeb\services\product
* @mixin \crmeb\services\product\storage\Copy
*/
class Product extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\product\\storage\\';
/**
* 默认驱动
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('product.default', 'copy');
}
/**
* 获取类的实例
* @param $class
* @return mixed|void
*/
protected function invokeClass($class)
{
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
$this->getConfigFile();
if (!$this->config) {
$this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
}
$handleAccessToken = new AccessTokenServeService($this->config['access_key'] ?? '', $this->config['secret_key'] ?? '');
$handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile]);
$this->config = [];
return $handle;
}
}

View File

@@ -0,0 +1,59 @@
<?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\product\storage;
use crmeb\basic\BaseProduct;
/**
* Class Copy
* @package crmeb\services\product\storage
*/
class Copy extends BaseProduct
{
/**
* 是否开通
*/
const PRODUCT_OPEN = 'v2/copy/open';
/**
* 获取详情
*/
const PRODUCT_GOODS = 'v2/copy/goods';
/** 初始化
* @param array $config
*/
protected function initialize(array $config = [])
{
parent::initialize($config);
}
/** 是否开通复制
* @return mixed
*/
public function open()
{
return $this->accessToken->httpRequest(self::PRODUCT_OPEN, []);
}
/** 复制商品
* @return mixed
*/
public function goods(string $url)
{
$param['url'] = $url;
return $this->accessToken->httpRequest(self::PRODUCT_GOODS, $param);
}
}

View File

@@ -0,0 +1,64 @@
<?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\serve;
use crmeb\basic\BaseManager;
use crmeb\services\AccessTokenServeService;
use crmeb\services\serve\storage\Crmeb;
use think\facade\Config;
use think\Container;
/**
* Class Serve
* @package crmeb\services\serve
* @mixin Crmeb
*/
class Serve extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\serve\\storage\\';
/**
* 默认驱动
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('serve.default', 'crmeb');
}
/**
* 获取类的实例
* @param $class
* @return mixed|void
*/
protected function invokeClass($class)
{
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
$this->getConfigFile();
if (!$this->config) {
$this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
}
$handleAccessToken = new AccessTokenServeService($this->config['access_key'] ?? '', $this->config['secret_key'] ?? '');
$handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile]);
$this->config = [];
return $handle;
}
}

View File

@@ -0,0 +1,75 @@
<?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\serve\storage;
use crmeb\basic\BaseStorage;
use crmeb\services\AccessTokenServeService;
use think\exception\ValidateException;
/**
* Class Crmeb
* @package crmeb\services\serve\storage
*/
class Crmeb extends BaseStorage
{
protected $accessToken;
/**
* Crmeb constructor.
* @param string $name
* @param AccessTokenServeService $service
* @param string|null $configFile
*/
public function __construct(string $name, AccessTokenServeService $service, string $configFile = null)
{
$this->accessToken = $service;
}
/**
* 获取用户信息
* @param string $account
* @param string $secret
* @return array|mixed
*/
public function getUser()
{
return $this->accessToken->httpRequest('v2/user/info');
}
/**
* 用量记录
* @param int $page
* @param int $limit
* @param int $type
* @return array|mixed
*/
public function record(int $page, int $limit, int $type, $status = '')
{
$typeContent = [1 => 'sms', 2 => 'expr_dump', 3 => 'expr_query', 4 => 'copy'];
if (!isset($typeContent[$type])) {
throw new ValidateException('参数类型不正确');
}
$data = ['page' => $page, 'limit' => $limit, 'type' => $typeContent[$type]];
if ($type == 1 && $status != '') {
$data['status'] = $status;
}
return $this->accessToken->httpRequest('user/record', $data);
}
protected function initialize(array $config)
{
}
}

View File

@@ -0,0 +1,65 @@
<?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\sms;
use crmeb\basic\BaseManager;
use crmeb\services\AccessTokenServeService;
use crmeb\services\sms\storage\Yihaotong;
use think\Container;
use think\facade\Config;
/**
* Class Sms1
* @package crmeb\services\sms
* @mixin Yihaotong
*/
class Sms extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\sms\\storage\\';
/**
* 默认驱动
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('sms.default', 'yunxin');
}
/**
* 获取类的实例
* @param $class
* @return mixed|void
*/
protected function invokeClass($class)
{
if (!class_exists($class)) {
throw new \RuntimeException('class not exists: ' . $class);
}
$this->getConfigFile();
if (!$this->config) {
$this->config = Config::get($this->configFile . '.stores.' . $this->name, []);
}
$handleAccessToken = new AccessTokenServeService($this->config['sms_account'] ?? '', $this->config['sms_token'] ?? '');
$handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile, $this->config]);
$this->config = [];
return $handle;
}
}

View File

@@ -0,0 +1,119 @@
<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace crmeb\services\sms\storage;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Models\SendSmsRequest;
use AlibabaCloud\Tea\Exception\TeaError;
use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
use crmeb\basic\BaseSmss;
use crmeb\exceptions\ApiException;
use Darabonba\OpenApi\Models\Config as AliConfig;
use think\exception\ValidateException;
use think\facade\Config;
use think\facade\Log;
/**
* Class Aliyun
* @package crmeb\services\sms\storage
*/
class Aliyun extends BaseSmss
{
protected $AccessKeyId = '';
protected $AccessKeySecret = '';
protected $SignName = '';
/**
* @param array $config
* @return mixed|void
*/
protected function initialize(array $config = [])
{
parent::initialize($config);
$this->SignName = $config['aliyun_SignName'] ?? '';
$this->AccessKeyId = $config['aliyun_AccessKeyId'] ?? '';
$this->AccessKeySecret = $config['aliyun_AccessKeySecret'] ?? '';
}
/**
* 发送短信
* @param string $phone
* @param string $templateId
* @param array $data
* @return array|bool|mixed
*/
public function send(string $phone, string $templateId, array $data = [])
{
if (empty($phone)) {
return $this->setError('电话号码不能为空');
}
$config = new AliConfig([
"accessKeyId" => $this->AccessKeyId,
"accessKeySecret" => $this->AccessKeySecret
]);
// 访问的域名
$config->endpoint = "dysmsapi.aliyuncs.com";
$client = new Dysmsapi($config);
if (!$templateId) throw new ApiException('模板不存在:' . $templateId);
$sendSmsRequest = new SendSmsRequest([
"phoneNumbers" => $phone,
"signName" => $this->SignName,
"templateCode" => $templateId,
"templateParam" => json_encode($data),
]);
$runtime = new RuntimeOptions([]);
try {
// 复制代码运行请自行打印 API 的返回值
$resp = $client->sendSmsWithOptions($sendSmsRequest, $runtime);
if (isset($resp) && $resp->body->code !== 'OK') {
throw new ApiException('【阿里云平台错误提示】:' . $resp->body->message);
}
return [
'id' => $resp->body->requestId,
'content' => json_encode($data),
'template' => $templateId,
];
} catch (\Exception $e) {
throw new ApiException('【阿里云平台错误提示】:' . $e->getMessage());
}
}
public function open()
{
}
public function modify(string $sign = null, string $phone, string $code)
{
}
public function info()
{
}
public function temps(int $page = 0, int $limit = 10, int $type = 1)
{
}
public function apply(string $title, string $content, int $type)
{
}
public function applys(int $tempType, int $page, int $limit)
{
}
public function record($record_id)
{
}
}

View File

@@ -0,0 +1,262 @@
<?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\sms\storage;
use crmeb\basic\BaseSmss;
use crmeb\services\HttpService;
use think\exception\ValidateException;
use think\facade\Config;
/**
* Class Yunxin
* @package crmeb\services\sms\storage
*/
class Yihaotong extends BaseSmss
{
/**
* 开通
*/
const SMS_OPEN = 'v2/sms_v2/open';
/**
* 修改签名
*/
const SMS_MODIFY = 'v2/sms_v2/modify';
/**
* 用户信息
*/
const SMS_INFO = 'v2/sms_v2/info';
/**
* 发送短信
*/
const SMS_SEND = 'v2/sms_v2/send';
/**
* 短信模板
*/
const SMS_TEMPS = 'v2/sms_v2/temps';
/**
* 申请模板
*/
const SMS_APPLY = 'v2/sms_v2/apply';
/**
* 模板记录
*/
const SMS_APPLYS = 'v2/sms_v2/applys';
/**
* 发送记录
*/
const SMS_RECORD = 'v2/sms_v2/record';
/**
* 获取短信发送状态
*/
const SMS_STSTUS = 'v2/sms/status';
/**
* 短信签名
* @var string
*/
protected $sign = '';
/**
* 模板id
* @var array
*/
protected $templateIds = [];
/** 初始化
* @param array $config
*/
protected function initialize(array $config = [])
{
parent::initialize($config);
$this->templateIds = Config::get($this->configFile . '.stores.' . $this->name . '.template_id', []);
}
/**
* 提取模板code
* @param string $templateId
* @return null
*/
protected function getTemplateCode(string $templateId)
{
return $this->templateIds[$templateId] ?? null;
}
/**
* 设置签名
* @param $sign
* @return $this
*/
public function setSign($sign)
{
$this->sign = $sign;
return $this;
}
/**
* 获取验证码
* @param string $phone
* @return array|mixed
*/
public function captcha(string $phone)
{
$params = [
'phone' => $phone
];
return $this->accessToken->httpRequest('sms/captcha', $params, 'GET', false);
}
/**
* 开通服务
* @return array|bool|mixed
*/
public function open()
{
$param = [
'sign' => $this->sign
];
return $this->accessToken->httpRequest(self::SMS_OPEN, $param);
}
/**
* 修改签名
* @param string $sign
* @return array|bool|mixed
*/
public function modify(string $sign = null, string $phone, string $code)
{
$param = [
'sign' => $sign ?: $this->sign,
'verify_code' => $code,
'phone' => $phone
];
return $this->accessToken->httpRequest(self::SMS_MODIFY, $param);
}
/**
* 获取用户信息
* @return array|bool|mixed
*/
public function info()
{
return $this->accessToken->httpRequest(self::SMS_INFO, []);
}
/**
* 获取短信模板
* @param int $page
* @param int $limit
* @param int $type
* @return array|mixed
*/
public function temps(int $page = 0, int $limit = 10, int $type = 1)
{
$param = [
'page' => $page,
'limit' => $limit,
'temp_type' => $type
];
return $this->accessToken->httpRequest(self::SMS_TEMPS, $param);
}
/**
* 申请模版
* @param $title
* @param $content
* @param $type
* @return array|bool|mixed
*/
public function apply(string $title, string $content, int $type)
{
$param = [
'title' => $title,
'content' => $content,
'type' => $type
];
return $this->accessToken->httpRequest(self::SMS_APPLY, $param);
}
/**
* 申请记录
* @param $temp_type
* @param int $page
* @param int $limit
* @return array|bool|mixed
*/
public function applys(int $tempType, int $page, int $limit)
{
$param = [
'temp_type' => $tempType,
'page' => $page,
'limit' => $limit
];
return $this->accessToken->httpRequest(self::SMS_APPLYS, $param);
}
/**
* 发送短信
* @param $phone
* @param $template
* @param $param
* @return bool|string
*/
public function send(string $phone, string $templateId, array $data = [])
{
if (!$phone) {
throw new ValidateException('手机号不能为空');
}
$param = [
'phone' => $phone,
'host' => request()->host()
];
$param['temp_id'] = $templateId;
if (!$param['temp_id']) {
throw new ValidateException('模版ID不存在');
}
$param['param'] = json_encode($data);
return $this->accessToken->httpRequest(self::SMS_SEND, $param);
}
/**
* 发送记录
* @param $record_id
* @return array|bool|mixed
*/
public function record($record_id)
{
$param = [
'record_id' => $record_id
];
return $this->accessToken->httpRequest(self::SMS_RECORD, $param);
}
/**
* 获取发送状态
* @param array $recordIds
* @return array|mixed
*/
public function getStatus(array $recordIds)
{
$data['record_id'] = json_encode($recordIds);
return $this->accessToken->httpRequest(self::SMS_STSTUS, $data, 'POST', false);
}
}

View File

@@ -0,0 +1,40 @@
<?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\template;
use crmeb\basic\BaseManager;
use think\facade\Config;
/**
* Class Template
* @package crmeb\services\template
* @mixin \crmeb\services\template\storage\Wechat
* @mixin \crmeb\services\template\storage\Subscribe
*/
class Template extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\template\\storage\\';
/**
* 设置默认
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('template.default', 'wechat');
}
}

View File

@@ -0,0 +1,68 @@
<?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\template\storage;
use crmeb\basic\BaseMessage;
class Baidu extends BaseMessage
{
/**
* 初始化
* @param array $config
* @return mixed|void
*/
protected function initialize(array $config)
{
parent::initialize($config); //
}
/**
* 发送模板消息
* @param string $templateId
* @param array $data
* @return mixed|void
*/
public function send(string $templateId, array $data = [])
{
//
}
/**
* 添加模板消息
* @param string $shortId
* @return mixed|void
*/
public function add(string $shortId)
{
//
}
/**
* 删除模板消息
* @param string $templateId
* @return mixed|void
*/
public function delete(string $templateId)
{
//
}
/**
* 模板消息列表
* @return mixed|void
*/
public function list()
{
//
}
}

View File

@@ -0,0 +1,88 @@
<?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\template\storage;
use app\services\message\TemplateMessageServices;
use crmeb\basic\BaseMessage;
use crmeb\services\CacheService;
use crmeb\services\wechat\MiniProgram;
use think\facade\Log;
/**
* 订阅消息
* Class Subscribe
* @package crmeb\services\template\storage
*/
class Subscribe extends BaseMessage
{
protected function initialize(array $config)
{
parent::initialize($config); //
}
/**
* @param string $templateId
* @return mixed
*/
public function getTempId(string $templateId)
{
/** @var TemplateMessageServices $services */
$services = app()->make(TemplateMessageServices::class);
return CacheService::handler('TEMPLATE')->remember('subscribe_' . $templateId, function () use ($services, $templateId) {
return $services->getTempId($templateId);
});
}
/**
* 发送订阅消息
* @param string $templateId
* @param array $data
* @return bool|\EasyWeChat\Support\Collection|mixed|null
*/
public function send(string $templateId, array $data = [])
{
$templateId = $this->getTemplateCode($templateId);
if (!$templateId) {
return $this->setError('Template number does not exist');
}
$tempid = $this->getTempId($templateId);
if (!$tempid) {
return $this->setError('Template ID does not exist');
}
if (!$this->openId) {
return $this->setError('Openid does not exist');
}
try {
$res = MiniProgram::sendSubscribeTemlate($this->openId, $tempid, $data, $this->toUrl);
$this->clear();
return $res;
} catch (\Throwable $e) {
$this->isLog() && Log::error('发送给openid为:' . $this->openId . '小程序订阅消息失败,模板id为:' . $tempid . ';错误原因为:' . $e->getMessage());
return $this->setError($e->getMessage());
}
}
public function delete(string $templateId)
{
//
}
public function add(string $shortId)
{
//
}
public function list()
{
//
}
}

View File

@@ -0,0 +1,123 @@
<?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\template\storage;
use app\services\message\TemplateMessageServices;
use crmeb\basic\BaseMessage;
use crmeb\services\CacheService;
use crmeb\services\wechat\OfficialAccount;
use EasyWeChat\Kernel\Support\Collection;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use think\facade\Log;
/**
* 公众号模板消息
* Class Wechat
* @package crmeb\services\template\storage
*/
class Wechat extends BaseMessage
{
/**
* 初始化
* @param array $config
* @return mixed|void
*/
protected function initialize(array $config)
{
parent::initialize($config);
}
/**
* @param string $templateId
* @return mixed
* @throws \throwable
*/
public function getTempId(string $templateId)
{
/** @var TemplateMessageServices $services */
$services = app()->make(TemplateMessageServices::class);
return CacheService::handler('TEMPLATE')->remember('wechat_' . $templateId, function () use ($services, $templateId) {
return $services->getTempId($templateId, 1);
});
}
/**
* 发送消息
* @param string $templateId
* @param array $data
* @return bool|mixed
* @throws GuzzleException
*/
public function send(string $templateId, array $data = [])
{
$templateId = $this->getTemplateCode($templateId);
if (!$templateId) {
return $this->setError('Template number does not exist');
}
$tempid = $this->getTempId($templateId);
if (!$tempid) {
return $this->setError('Template ID does not exist');
}
if (!$this->openId) {
return $this->setError('Openid does not exist');
}
try {
$res = OfficialAccount::sendTemplate($this->openId, $tempid, $data, $this->toUrl, $this->color);
$this->clear();
return $res;
} catch (\Exception $e) {
$this->isLog() && Log::error('发送给openid为:' . $this->openId . '微信模板消息失败,模板id为:' . $tempid . ';错误原因为:' . $e->getMessage());
return $this->setError($e->getMessage());
}
}
/**
* 获取所有模板
* @return array|Collection|mixed|object|ResponseInterface|string
* @throws GuzzleException
*/
public function list()
{
return OfficialAccount::getPrivateTemplates();
}
/**
* 添加模板消息
* @param string $shortId
* @return array|Collection|mixed|object|ResponseInterface|string
* @throws GuzzleException
*/
public function add(string $shortId)
{
return OfficialAccount::addTemplateId($shortId);
}
/**
* 删除模板消息
* @param string $templateId
* @return array|Collection|mixed|object|ResponseInterface|string
* @throws GuzzleException
*/
public function delete(string $templateId)
{
return OfficialAccount::deleleTemplate($templateId);
}
/**
* 返回所有支持的行业列表
* @return array|Collection|object|ResponseInterface|string
*/
public function getIndustry()
{
return OfficialAccount::getIndustry();
}
}

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\upload;
/**
* 基础请求
* Class BaseClient
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
* @package crmeb\services\upload
*/
abstract class BaseClient
{
/**
* 是否解析为xml
* @var bool
*/
protected $isXml = true;
/**
*
* @var []callable
*/
protected $curlFn = [];
/**
* @param callable $curlFn
* @return $this
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function middleware(callable $curlFn)
{
$this->curlFn[] = $curlFn;
return $this;
}
/**
* 发起请求
* @param string $url
* @param string $method
* @param array $data
* @param array $clientHeader
* @param int $timeout
* @return array|bool|SimpleXMLElement|mixed
*/
protected function requestClient(string $url, string $method, array $data = [], array $clientHeader = [], int $timeout = 10)
{
$headers = [];
foreach ($clientHeader as $key => $item) {
$headers[] = $key . ':' . $item;
}
$curl = curl_init($url);
//请求方式
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
//post请求
if (!empty($data['body'])) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $data['body']);
} else if (!empty($data['json'])) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data['json']));
} else {
$curlFn = $this->curlFn;
foreach ($curlFn as $item) {
if ($item instanceof \Closure) {
$curlFn($curl);
}
}
}
//超时时间
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
//设置header头
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_FAILONERROR, false);
//返回抓取数据
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
//输出header头信息
curl_setopt($curl, CURLOPT_HEADER, true);
//TRUE 时追踪句柄的请求字符串,从 PHP 5.1.3 开始可用。这个很关键,就是允许你查看请求header
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
//https请求
if (1 == strpos("$" . $url, "https://")) {
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
}
[$content, $status] = [curl_exec($curl), curl_getinfo($curl)];
curl_close($curl);
$content = trim(substr($content, $status['header_size']));
if ($content) {
if ($this->isXml) {
return XML::parse($content);
} else {
return json_decode($content, true);
}
} else {
return true;
}
}
}

View File

@@ -0,0 +1,46 @@
<?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\upload;
use think\facade\Config;
use crmeb\basic\BaseManager;
use crmeb\services\upload\storage\COS;
use crmeb\services\upload\storage\Local;
use crmeb\services\upload\storage\OSS;
use crmeb\services\upload\storage\Qiniu;
/**
* Class Upload
* @package crmeb\services\upload
* @mixin Local
* @mixin OSS
* @mixin COS
* @mixin Qiniu
*/
class Upload extends BaseManager
{
/**
* 空间名
* @var string
*/
protected string $namespace = '\\crmeb\\services\\upload\\storage\\';
/**
* 设置默认上传类型
* @return mixed
*/
protected function getDefaultDriver()
{
return Config::get('upload.default', 'local');
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace crmeb\services\upload;
class XML
{
/**
* XML to array.
*
* @param string $xml XML string
*
* @return array
*/
public static function parse($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(
$data,
$root = 'xml',
$item = 'item',
$attr = '',
$id = 'id'
)
{
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)
{
return sprintf('<![CDATA[%s]]>', $string);
}
/**
* Object to array.
*
*
* @param SimpleXMLElement $obj
*
* @return array
*/
protected static function normalize($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($data, $item = 'item', $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($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,576 @@
<?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\upload\extend\cos;
use crmeb\exceptions\UploadException;
use crmeb\services\wechat\util\XML;
/**
* Class Client
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
* @package crmeb\services\upload\extend\cos
*/
class Client
{
/**
* @var string
*/
protected $accessKey;
/**
* @var string
*/
protected $secretKey;
/**
* @var string
*/
protected $appid;
/**
* @var mixed|string
*/
protected $bucket;
/**
* @var mixed|string
*/
protected $region;
/**
* @var mixed|string
*/
protected $uploadUrl;
/**
* @var string
*/
protected $action = '';
/**
* @var array
*/
protected $response = ['content' => null, 'code' => 200, 'header' => []];
/**
* @var array
*/
protected $request = ['header' => [], 'body' => [], 'host' => ''];
/**
* @var string
*/
protected $cosacl = 'public-read';
/**
* Client constructor.
* @param array $config
*/
public function __construct(array $config)
{
$this->accessKey = $config['accessKey'] ?? '';
$this->secretKey = $config['secretKey'] ?? '';
$this->appid = $config['appid'] ?? '';
$this->bucket = $config['bucket'] ?? '';
$this->region = $config['region'] ?? 'ap-chengdu';
$this->uploadUrl = $config['uploadUrl'] ?? '';
}
/**
* 获取实际请求
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function getResponse()
{
$response = $this->response;
$this->response = ['content' => null, 'http_code' => 200, 'header' => []];
return $response;
}
/**
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function getRequest()
{
$request = $this->request;
$this->request = ['header' => [], 'body' => [], 'host' => ''];
return $request;
}
/**
* 拼接请求地址
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
*/
protected function makeUpUrl()
{
return $this->bucket . '.cos.' . $this->region . '.myqcloud.com';
}
/**
* @return bool
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
*/
protected function ssl()
{
return strstr($this->uploadUrl, 'https://') !== false;
}
/**
* 检查参数
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
*/
protected function checkOptions()
{
if (!$this->bucket) {
throw new UploadException('请传入桶名');
}
if (!$this->region) {
throw new UploadException('请传入所属地域');
}
if (!$this->accessKey) {
throw new UploadException('请传入SecretId');
}
if (!$this->secretKey) {
throw new UploadException('请传入SecretKey');
}
}
/**
* 上传文件
* @param string $key
* @param $body
* @return string[]
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
*/
public function putObject(string $key, $body)
{
$this->checkOptions();
$url = $this->makeUpUrl();
$header = [
'Content-Type' => 'image/jpeg',
'x-cos-acl' => $this->cosacl,
'Content-MD5' => base64_encode(md5($body, true)),
'Host' => $url
];
$imageUrl = ($this->ssl() ? 'https://' : 'http://') . $url . '/' . $key;
$res = $this->request($imageUrl, 'PUT', ['body' => $body], $header);
if ($res && !empty($res['Message'])) {
throw new UploadException($res['Message']);
}
return [
'name' => $key,
'path' => $imageUrl
];
}
/**
* 删除文件
* @param string $bucket
* @param string $key
* @return array|false
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/19
*/
public function deleteObject(string $bucket, string $key)
{
$url = $this->getRequestHost($bucket);
$header = [
'Host' => $url
];
$res = $this->request('https://' . $url . '/' . $key, 'delete', [], $header);
if ($res && !empty($res['Message'])) {
throw new UploadException($res['Message']);
}
return $res;
}
/**
* 获取桶列表
* @return array|false|\SimpleXMLElement|string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/19
*/
public function listBuckets()
{
$url = 'service.cos.myqcloud.com';
$header = [
'Host' => $url
];
$res = $this->request('https://' . $url . '/', 'get', [], $header);
if ($res && !empty($res['Message'])) {
throw new UploadException($res['Message']);
}
return $res;
}
/**
* 检测桶,不存在返回true
* @param string $bucket
* @param string $region
* @return bool
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function headBucket(string $bucket, string $region = '')
{
$url = $this->getRequestHost($bucket, $region);
$header = [
'Host' => $url
];
$this->request('https://' . $url, 'head', [], $header);
$response = $this->getResponse();
return $response['code'] == 404;
}
/**
* 创建桶
* @param string $bucket
* @param string $region
* @param string $acl
* @return array|false|\SimpleXMLElement|string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function createBucket(string $bucket, string $region = '', string $acl = 'public-read')
{
return $this->noBodyRequest('put', $bucket, $region, $acl);
}
/**
* 组合成xml
* @param array $data
* @param string $root
* @param string $itemKey
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
protected function xmlBuild(array $xmlAttr, string $root = 'xml', string $itemKey = 'item')
{
$xml = '<' . $root . '>';
$xml .= '<' . $itemKey . '>';
foreach ($xmlAttr as $kk => $vv) {
if (is_array($vv)) {
foreach ($vv as $v) {
$xml .= '<' . $kk . '>' . $v . '</' . $kk . '>';
}
} else {
$xml .= '<' . $kk . '>' . $vv . '</' . $kk . '>';
}
}
$xml .= '</' . $itemKey . '>';
$xml .= '</' . $root . '>';
return $xml;
}
/**
* 设置跨域
* @param string $bucket
* @param string $region
* @param array $data
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function putBucketCors(string $bucket, array $data, string $region = '')
{
$url = $this->getRequestHost($bucket, $region);
$xml = $this->xmlBuild($data, 'CORSConfiguration', 'CORSRule');
$header = [
'Host' => $url,
'Content-Type' => 'application/xml',
'Content-Length' => strlen($xml),
'Content-MD5' => base64_encode(md5($xml, true))
];
$res = $this->request('https://' . $url . '/?cors', 'put', ['xml' => $xml], $header);
if ($res && !empty($res['Message'])) {
throw new UploadException($res['Message']);
}
return $res;
}
/**
* 删除
* @param string $name
* @param string $region
* @return array|false|\SimpleXMLElement|string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function deleteBucket(string $name, string $region = '')
{
return $this->noBodyRequest('delete', $name, $region);
}
/**
* 获取桶下的
* @param string $name
* @param string $region
* @return array|false|\SimpleXMLElement|string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function getBucketDomain(string $name, string $region = '')
{
$this->action = 'domain';
return $this->noBodyRequest('get', $name, $region);
}
/**
* 绑定域名
* @param string $bucket
* @param string $region
* @param array $data
* @return array|false|\SimpleXMLElement|string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/19
*/
public function putBucketDomain(string $bucket, string $region, array $data)
{
$url = $this->getRequestHost($bucket, $region);
$xml = $this->xmlBuild($data, 'DomainConfiguration', 'DomainRule');
$header = [
'Host' => $url,
'Content-Type' => 'application/xml',
'Content-Length' => strlen($xml),
'Content-MD5' => base64_encode(md5($xml, true))
];
$res = $this->request('https://' . $url . '/?domain', 'put', ['xml' => $xml], $header);
if ($res && !empty($res['Message'])) {
throw new UploadException($res['Message']);
}
return $res;
}
/**
* @param string $bucket
* @param string $region
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
protected function getRequestHost(string $bucket, string $region = '')
{
if (!$this->accessKey) {
throw new UploadException('请传入SecretId');
}
if (!$this->secretKey) {
throw new UploadException('请传入SecretKey');
}
if (strstr($bucket, '-') === false) {
$bucket = $bucket . '-' . $this->appid;
}
return $bucket . '.cos.' . ($region ?: $this->region) . '.myqcloud.com';
}
/**
* @param string $method
* @param string $bucket
* @param string $region
* @param string|null $acl
* @return array|false|\SimpleXMLElement|string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
public function noBodyRequest(string $method, string $bucket, string $region = '', string $acl = null, bool $isExc = true)
{
$url = $this->getRequestHost($bucket, $region);
$header = [
'Host' => $url
];
if ($acl) {
$header['x-cos-acl'] = $acl;
}
if (in_array($method, ['put', 'post'])) {
$header['Content-Length'] = 0;
}
$res = $this->request('https://' . $url . '/' . ($this->action ? '?' . $this->action : ''), $method, [], $header);
$this->action = '';
if ($isExc) {
if ($res && !empty($res['Message'])) {
throw new UploadException($res['Message']);
}
}
return $res;
}
/**
* 发起请求
* @param string $url
* @param string $method
* @param array $data
* @param array $header
* @param int $timeout
* @return array|false|\SimpleXMLElement|string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
*/
public function request(string $url, string $method, array $data, array $header = [], int $timeout = 5)
{
$this->request['body'] = $data;
$this->request['host'] = $url;
$urlAttr = parse_url($url);
$curl = curl_init($url);
$method = strtoupper($method);
//请求方式
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
//超时时间
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
//设置header头
$header = array_merge($header, $this->getSign($url, $method, $urlAttr['path'] ?? '', [], $header));
$this->request['header'] = $header;
$clientHeader = [];
foreach ($header as $key => $item) {
$clientHeader[] = $key . ':' . $item;
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $clientHeader);
curl_setopt($curl, CURLOPT_FAILONERROR, false);
//返回抓取数据
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
//输出header头信息
curl_setopt($curl, CURLOPT_HEADER, true);
//TRUE 时追踪句柄的请求字符串,从 PHP 5.1.3 开始可用。这个很关键,就是允许你查看请求header
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
//https请求
if (1 == strpos("$" . $url, "https://")) {
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
}
//post请求
if ($method == 'PUT' && !empty($data['body'])) {
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
// 注意这里的'file'是上传地址指定的key名
curl_setopt($curl, CURLOPT_POSTFIELDS, $data['body']);
}
if (!empty($data['xml'])) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $data['xml']);
}
[$content, $status] = [curl_exec($curl), curl_getinfo($curl), curl_close($curl)];
$content = trim(substr($content, $status['header_size']));
$this->response['content'] = $content;
$this->response['code'] = $status['http_code'];
$this->response['header'] = $status;
$res = XML::parse($content);
if ($res) {
return $res;
}
return (intval($status["http_code"]) === 200) ? $content : false;
}
/**
* 获取签名
* @param string $method
* @param string $urlPath
* @param array $query
* @param array $headers
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/27
*/
public function getSign(string $url, string $method, string $urlPath, array $query = [], array $headers = [])
{
return (new Signature($this->accessKey, $this->secretKey, ['signHost' => $url]))->signRequest($method, $urlPath, $query, $headers);
}
}

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\upload\extend\cos;
class Scope
{
protected $action;
protected $bucket;
protected $region;
protected $resourcePrefix;
protected $effect = 'allow';
public function __construct($action, $bucket, $region, $resourcePrefix)
{
$this->action = $action;
$this->bucket = $bucket;
$this->region = $region;
$this->resourcePrefix = $resourcePrefix;
}
public function set_effect($isAllow)
{
if ($isAllow) {
$this->effect = 'allow';
} else {
$this->effect = 'deny';
}
}
public function get_action()
{
if ($this->action == null) {
throw new \Exception("action == null");
}
return $this->action;
}
public function get_resource()
{
if ($this->bucket == null) {
throw new \Exception("bucket == null");
}
if ($this->region == null) {
throw new \Exception("region == null");
}
if ($this->resourcePrefix == null) {
throw new \Exception("resourcePrefix == null");
}
$index = strripos($this->bucket, '-');
if ($index < 0) {
throw new Exception("bucket is invalid: " . $this->bucket);
}
$appid = substr($this->bucket, $index + 1);
if (!(strpos($this->resourcePrefix, '/') === 0)) {
$this->resourcePrefix = '/' . $this->resourcePrefix;
}
return 'qcs::cos:' . $this->region . ':uid/' . $appid . ':' . $this->bucket . $this->resourcePrefix;
}
public function get_effect()
{
return $this->effect;
}
}

View File

@@ -0,0 +1,213 @@
<?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\upload\extend\cos;
/**
* Class 生成签名
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/26
* @package crmeb\services\upload\extend\cos
*/
class Signature
{
/**
* @var string
*/
private $accessKey;
/**
* @var string
*/
private $secretKey;
/**
* @var array
*/
private $options;
/**
* Signature constructor.
* @param string $accessKey
* @param string $secretKey
* @param array $options
* @param string $token
*/
public function __construct(string $accessKey, string $secretKey, array $options = [], string $token = '')
{
$this->accessKey = $accessKey;
$this->secretKey = $secretKey;
$this->options = $options;
$this->token = $token;
$this->signHeader = [
'cache-control',
'content-disposition',
'content-encoding',
'content-length',
'content-md5',
'content-type',
'expect',
'expires',
'host',
'if-match',
'if-modified-since',
'if-none-match',
'if-unmodified-since',
'origin',
'range',
'response-cache-control',
'response-content-disposition',
'response-content-encoding',
'response-content-language',
'response-content-type',
'response-expires',
'transfer-encoding',
'versionid',
];
date_default_timezone_set('Asia/Shanghai');
}
public function needCheckHeader($header)
{
if ($this->startWith($header, 'x-cos-')) {
return true;
}
if (in_array($header, $this->signHeader)) {
return true;
}
return false;
}
/**
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
* @param $haystack
* @param $needle
* @return bool
*/
protected function startWith($haystack, $needle)
{
$length = strlen($needle);
if ($length == 0) {
return true;
}
return (substr($haystack, 0, $length) === $needle);
}
/**
* @param string $method
* @param string $urlPath
* @param array $querys
* @param array $headers
* @return string[]
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/26
*/
public function signRequest(string $method, string $urlPath, array $querys = [], array $headers = [])
{
$authorization = $this->createAuthorization($method, $urlPath, $querys, $headers);
return ['Authorization' => $authorization];
}
/**
* @param string $method
* @param string $urlPath
* @param array $querys
* @param array $headers
* @param string $expires
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/26
*/
public function createAuthorization(string $method, string $urlPath, array $querys = [], array $headers = [], $expires = '+30 minutes')
{
if (is_null($expires) || !strtotime($expires)) {
$expires = '+30 minutes';
}
$signTime = ( string )(time() - 60) . ';' . ( string )(strtotime($expires));
$urlParamListArray = [];
foreach ($querys as $query) {
if (!empty($query)) {
$tmpquery = explode('=', $query);
//为了保证CI的key中有=号的情况也能正常通过,ci在这层之前已经encode了,这里需要拆开重新encode,防止上方explode拆错
$key = strtolower(rawurlencode(urldecode($tmpquery[0])));
if (count($tmpquery) >= 2) {
$value = $tmpquery[1];
} else {
$value = "";
}
//host开关
if (!$this->options['signHost'] && $key == 'host') {
continue;
}
$urlParamListArray[$key] = $key . '=' . $value;
}
}
ksort($urlParamListArray);
$urlParamList = join(';', array_keys($urlParamListArray));
$httpParameters = join('&', array_values($urlParamListArray));
$headerListArray = [];
foreach ($headers as $key => $value) {
$key = strtolower(urlencode($key));
$value = rawurlencode($value);
if (!$this->options['signHost'] && $key == 'host') {
continue;
}
if ($this->needCheckHeader($key)) {
$headerListArray[$key] = $key . '=' . $value;
}
}
ksort($headerListArray);
$headerList = join(';', array_keys($headerListArray));
$httpHeaders = join('&', array_values($headerListArray));
$httpString = strtolower($method) . "\n" . urldecode($urlPath) . "\n" . $httpParameters .
"\n" . $httpHeaders . "\n";
$sha1edHttpString = sha1($httpString);
$stringToSign = "sha1\n$signTime\n$sha1edHttpString\n";
$signKey = hash_hmac('sha1', $signTime, trim($this->secretKey));
$signature = hash_hmac('sha1', $stringToSign, $signKey);
$authorization = 'q-sign-algorithm=sha1&q-ak=' . trim($this->accessKey) .
"&q-sign-time=$signTime&q-key-time=$signTime&q-header-list=$headerList&q-url-param-list=$urlParamList&" .
"q-signature=$signature";
return $authorization;
}
/**
* @param string $url
* @param string $method
* @param string $urlPath
* @param array $querys
* @param array $headers
* @param string $expires
* @return string[]
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/26
*/
public function createPresignedUrl(string $url, string $method, string $urlPath, array $querys = [], array $headers = [], string $expires = '+30 minutes')
{
$authorization = $this->createAuthorization($method, $urlPath, $querys, $headers, $expires);
$uri = $url;
$query = 'sign=' . urlencode($authorization) . '&' . implode('&', $querys);
if ($this->token != null) {
$query = $query . '&x-cos-security-token=' . $this->token;
}
return [$uri, $query];
}
}

View File

@@ -0,0 +1,321 @@
<?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\upload\extend\cos;
class Sts
{
// 临时密钥计算样例
function _hex2bin($data)
{
$len = strlen($data);
return pack("H" . $len, $data);
}
// obj 转 query string
function json2str($obj, $notEncode = false)
{
ksort($obj);
$arr = array();
if (!is_array($obj)) {
throw new \Exception('$obj must be an array, the actual value is:' . json_encode($obj));
}
foreach ($obj as $key => $val) {
array_push($arr, $key . '=' . ($notEncode ? $val : rawurlencode($val)));
}
return join('&', $arr);
}
// 计算临时密钥用的签名
function getSignature($opt, $key, $method, $config)
{
$host = "sts.tencentcloudapi.com";
if (array_key_exists('domain', $config)) {
$host = $config['domain'];
}
if (array_key_exists('endpoint', $config)) {
$host = "sts." . $config['endpoint'];
}
$formatString = $method . $host . '/?' . $this->json2str($opt, 1);
$sign = hash_hmac('sha1', $formatString, $key);
$sign = base64_encode($this->_hex2bin($sign));
return $sign;
}
// v2接口的key首字母小写,v3改成大写,此处做了向下兼容
function backwardCompat($result)
{
if (!is_array($result)) {
throw new \Exception('$result must be an array, the actual value is:' . json_encode($result));
}
$compat = array();
foreach ($result as $key => $value) {
if (is_array($value)) {
$compat[lcfirst($key)] = $this->backwardCompat($value);
} elseif ($key == 'Token') {
$compat['sessionToken'] = $value;
} else {
$compat[lcfirst($key)] = $value;
}
}
return $compat;
}
// 获取临时密钥
function getTempKeys($config)
{
$result = null;
try {
if (array_key_exists('policy', $config)) {
$policy = $config['policy'];
} else {
if (array_key_exists('bucket', $config)) {
$ShortBucketName = substr($config['bucket'], 0, strripos($config['bucket'], '-'));
$AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
} else {
throw new \Exception("bucket== null");
}
if (array_key_exists('allowPrefix', $config)) {
if (!(strpos($config['allowPrefix'], '/') === 0)) {
$config['allowPrefix'] = '/' . $config['allowPrefix'];
}
} else {
throw new \Exception("allowPrefix == null");
}
if (!array_key_exists('region', $config)) {
throw new \Exception("region == null");
}
$policy = array(
'version' => '2.0',
'statement' => array(
array(
'action' => $config['allowActions'],
'effect' => 'allow',
'resource' => array(
'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . $config['allowPrefix']
)
)
)
);
}
$policyStr = str_replace('\\/', '/', json_encode($policy));
$Action = 'GetFederationToken';
$Nonce = rand(10000, 20000);
$Timestamp = time();
$Method = 'POST';
if (array_key_exists('durationSeconds', $config)) {
if (!(is_integer($config['durationSeconds']))) {
throw new \Exception("durationSeconds must be a int type");
}
}
$params = array(
'SecretId' => $config['secretId'],
'Timestamp' => $Timestamp,
'Nonce' => $Nonce,
'Action' => $Action,
'DurationSeconds' => $config['durationSeconds'],
'Version' => '2018-08-13',
'Name' => 'cos',
'Region' => $config['region'],
'Policy' => urlencode($policyStr)
);
$params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
$url = 'https://sts.tencentcloudapi.com/';
if (array_key_exists('url', $config)) {
$url = $config['url'];
}
if (!array_key_exists('url', $config) && array_key_exists('domain', $config)) {
$url = 'https://sts.' . $config['domain'];
}
if (array_key_exists('endpoint', $config)) {
$url = 'https://sts.' . $config['endpoint'];
}
$ch = curl_init($url);
if (array_key_exists('proxy', $config)) {
$config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
$result = curl_exec($ch);
if (curl_errno($ch)) $result = curl_error($ch);
curl_close($ch);
$result = json_decode($result, 1);
if (isset($result['Response'])) {
$result = $result['Response'];
if (isset($result['Error'])) {
throw new \Exception("get cam failed");
}
$result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
}
$result = $this->backwardCompat($result);
return $result;
} catch (\Exception $e) {
if ($result == null) {
$result = "error: " . $e->getMessage();
} else {
$result = json_encode($result);
}
throw new \Exception($result);
}
}
//申请角色授权
function getRoleCredential($config)
{
$result = null;
try {
if (array_key_exists('policy', $config)) {
$policy = $config['policy'];
} else {
if (array_key_exists('bucket', $config)) {
$ShortBucketName = substr($config['bucket'], 0, strripos($config['bucket'], '-'));
$AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
} else {
throw new \Exception("bucket== null");
}
if (array_key_exists('allowPrefix', $config)) {
if (!(strpos($config['allowPrefix'], '/') === 0)) {
$config['allowPrefix'] = '/' . $config['allowPrefix'];
}
} else {
throw new \Exception("allowPrefix == null");
}
if (!array_key_exists('region', $config)) {
throw new \Exception("region == null");
}
$policy = array(
'version' => '2.0',
'statement' => array(
array(
'action' => $config['allowActions'],
'effect' => 'allow',
'resource' => array(
'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . $config['allowPrefix']
)
)
)
);
}
if (array_key_exists('roleArn', $config)) {
$RoleArn = $config['roleArn'];
} else {
throw new \Exception("roleArn == null");
}
$policyStr = str_replace('\\/', '/', json_encode($policy));
$Action = 'AssumeRole';
$Nonce = rand(10000, 20000);
$Timestamp = time();
$Method = 'POST';
$ExternalId = "";
if (array_key_exists('externalId', $config)) {
$ExternalId = $config['externalId'];
}
if (array_key_exists('durationSeconds', $config)) {
if (!(is_integer($config['durationSeconds']))) {
throw new \Exception("durationSeconds must be a int type");
}
}
$params = array(
'SecretId' => $config['secretId'],
'Timestamp' => $Timestamp,
'RoleArn' => $RoleArn,
'Action' => $Action,
'Nonce' => $Nonce,
'DurationSeconds' => $config['durationSeconds'],
'Version' => '2018-08-13',
'RoleSessionName' => 'cos',
'Region' => $config['region'],
'ExternalId' => $ExternalId,
'Policy' => urlencode($policyStr)
);
$params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
$url = 'https://sts.internal.tencentcloudapi.com/';
if (array_key_exists('endpoint', $config)) {
$url = 'https://sts.' . $config['endpoint'];
}
$ch = curl_init($url);
if (array_key_exists('proxy', $config)) {
$config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
$result = curl_exec($ch);
if (curl_errno($ch)) $result = curl_error($ch);
curl_close($ch);
$result = json_decode($result, 1);
if (isset($result['Response'])) {
$result = $result['Response'];
if (isset($result['Error'])) {
throw new \Exception("get cam failed");
}
$result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
}
$result = $this->backwardCompat($result);
return $result;
} catch (\Exception $e) {
if ($result == null) {
$result = "error: " . $e->getMessage();
} else {
$result = json_encode($result);
}
throw new \Exception($result);
}
}
// get policy
function getPolicy($scopes)
{
if (!is_array($scopes)) {
return null;
}
$statements = [];
for ($i = 0, $counts = count($scopes); $i < $counts; $i++) {
$actions = [];
$resources = [];
$actions[] = $scopes[$i]->get_action();
$resources[] = $scopes[$i]->get_resource();
$statement = [
'action' => $actions,
'effect' => $scopes[$i]->get_effect(),
'resource' => $resources
];
$statements[] = $statement;
}
$policy = [
'version' => '2.0',
'statement' => $statements
];
return $policy;
}
}

View File

@@ -0,0 +1,444 @@
<?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\upload\extend\jdoss;
use crmeb\exceptions\UploadException;
use crmeb\services\upload\BaseClient;
use GuzzleHttp\Psr7\Utils;
/**
* 京东云上传
* Class Client
* @package crmeb\services\upload\extend\jdoss
*/
class Client extends BaseClient
{
const ALGORITHM_REQUEST = 'AWS4-HMAC-SHA256';
const BLACKLIST_HEADERS = [
'cache-control' => true,
'content-type' => true,
'content-length' => true,
'expect' => true,
'max-forwards' => true,
'pragma' => true,
'range' => true,
'te' => true,
'if-match' => true,
'if-none-match' => true,
'if-modified-since' => true,
'if-unmodified-since' => true,
'if-range' => true,
'accept' => true,
'authorization' => true,
'proxy-authorization' => true,
'from' => true,
'referer' => true,
'user-agent' => true,
'x-amzn-trace-id' => true,
'aws-sdk-invocation-id' => true,
'aws-sdk-retry' => true,
];
/**
* AK
* @var
*/
protected $accessKeyId;
/**
* SK
* @var
*/
protected $secretKey;
/**
* 桶名
* @var string
*/
protected $bucketName;
/**
* 地区
* @var string
*/
protected $region;
/**
* @var mixed|string
*/
protected $uploadUrl;
/**
* @var string
*/
protected $baseUrl = 's3.<REGION>.jdcloud-oss.com';
//默认地域
const DEFAULT_REGION = 'cn-north-1';
/**
* Client constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->accessKeyId = $config['accessKey'] ?? '';
$this->secretKey = $config['secretKey'] ?? '';
$this->bucketName = $config['bucket'] ?? '';
$this->region = $config['region'] ?? self::DEFAULT_REGION;
$this->uploadUrl = $config['uploadUrl'] ?? '';
}
/**
* 上传
* @param string $bucket
* @param string $region
* @param string $key
* @param array $data
* @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
*/
public function putObject(string $bucket, string $region, string $key, array $data)
{
$url = $this->getRequestUrl($bucket, $region);
$header = [
'Host' => $url,
];
if (isset($data['body'])) {
$header['Content-Length'] = strlen($data['body']);
}
return $this->request('https://' . $url . '/'. $key, 'PUT', $data, $header);
}
/**
* 删除文件
* @param string $bucket
* @param string $region
* @param string $key
* @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
*/
public function deleteObject(string $bucket, string $region, string $key)
{
$url = $this->getRequestUrl($bucket, $region);
$header = [
'Host' => $url,
];
return $this->request('https://' . $url . '/'. $key, 'DELETE', [], $header);
}
/**
* 获取桶列表
* @return array|bool|\crmeb\services\upload\SimpleXMLElement|mixed
*/
public function listBuckets()
{
$url = $this->getRequestUrl();
$header = [
'Host' => $url,
];
$res = $this->request('https://' . $url . '/', 'GET', [], $header);
return $res;
}
/**
* 检测桶,不存在返回true
* @param string $bucket
* @param string $region
* @return array|bool|\crmeb\services\upload\SimpleXMLElement|mixed
*/
public function headBucket(string $bucket, string $region = '')
{
$url = $this->getRequestUrl($bucket, $region);
$header = [
'Host' => $url
];
return $this->request('https://' . $url, 'head', [], $header);
}
/**
* 创建桶
* @param $name
* @param $region
* @param $acl
* @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
*/
public function createBucket($name, $region, $acl)
{
$url = $this->getRequestUrl($name, $region);
$header = [
'Host' => $url,
'x-amz-acl' => $acl
];
$res = $this->request('https://' . $url . '/', 'PUT', [], $header);
return $res;
}
/**
* 删除桶
* @param string $bucket
* @param string $region
* @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
*/
public function deleteBucket(string $bucket, string $region = '')
{
$url = $this->getRequestUrl($bucket, $region);
$header = [
'Host' => $url
];
return $this->request('https://' . $url . '/', 'DELETE', [], $header);
}
/**
* 设置桶跨域规则
* @param string $bucket
* @param string $region
* @param $data
* @return array|\crmeb\services\upload\extend\cos\SimpleXMLElement
*/
public function putBucketCors(string $bucket, string $region = '', $data = [])
{
$url = $this->getRequestUrl($bucket, $region);
$header = [
'Host' => $url,
'content-type' => 'application/xml'
];
return $this->request('https://' . $url . '/?cors', 'PUT', $data, $header);
}
/**
* 获取host
* @param string $bucket
* @param string $region
* @return string
*/
public function getRequestUrl(string $bucket = '', string $region = self::DEFAULT_REGION)
{
if (!$this->accessKeyId) {
throw new UploadException('请传入SecretId');
}
if (!$this->secretKey) {
throw new UploadException('请传入SecretKey');
}
return ($bucket ? $bucket . '.' : '') . 's3.' . $region . '.jdcloud-oss.com';
}
/**
* 发起请求
* @param string $url
* @param string $method
* @param array $data
* @param array $clientHeader
* @param int $timeout
* @return array|bool|\crmeb\services\upload\SimpleXMLElement|mixed
*/
protected function request(string $url, string $method, array $data = [], array $clientHeader = [], int $timeout = 10)
{
if (!isset($clientHeader['Content-Length'])) {
$clientHeader['Content-Length'] = 0;
}
$clientHeader['x-amz-date'] = gmdate('Ymd\THis\Z');
$clientHeader['x-amz-content-sha256'] = hash('sha256', $data['body'] ?? '', false);
$authorization = $this->generateAwsSignatureV4($data['region'] ?? self::DEFAULT_REGION, $url, $method, $clientHeader, $data);
$clientHeader['Authorization'] = $authorization;
return $this->requestClient($url, $method, $data, $clientHeader, $timeout);
}
/**
* 生成签名
* @param string $region
* @param string $url
* @param string $httpMethod
* @param array $header
* @param array $data
* @param string $service
* @return string
*/
protected function generateAwsSignatureV4(string $region, string $url, string $httpMethod, array $header, array $data = [], string $service = 's3')
{
$algorithm = self::ALGORITHM_REQUEST;
$t = new \DateTime('UTC');
$amzDate = $t->format('Ymd\THis\Z');
$dateStamp = $t->format('Ymd');
[$canonicalRequest, $signedHeaders] = $this->createCanonicalRequest($url, $httpMethod, $header, $data);
$credentialScope = $dateStamp . '/' . $region . '/' . $service . '/aws4_request';
$stringToSign = $algorithm . "\n" . $amzDate . "\n" . $credentialScope . "\n" . hash('sha256', $canonicalRequest);
$signingKey = hash_hmac('sha256', 'aws4_request',
hash_hmac('sha256', $service,
hash_hmac('sha256', $region,
hash_hmac('sha256', $dateStamp, 'AWS4' . $this->secretKey, true),
true),
true),
true);
$signature = hash_hmac('sha256', $stringToSign, $signingKey);
return $algorithm . ' Credential=' . $this->accessKeyId . '/' . $credentialScope . ', SignedHeaders=' . $signedHeaders . ', Signature=' . $signature;
}
/**
* @param string $url
* @param string $httpMethod
* @param array $header
* @param array $data
* @return array
*/
public function createCanonicalRequest(string $url, string $httpMethod, array $header, array $data = [])
{
$canonicalQueryString = '';
$payload = '';
if (!empty($data['query'])) {
$query = $payload = $data['query'];
ksort($query);
$queryAttr = [];
foreach ($query as $key => $item) {
$queryAttr[urlencode($key)] = urlencode($item);
}
if ($queryAttr) {
$canonicalQueryString = implode('&', $queryAttr);
}
$payload = json_encode($payload);
} elseif (!empty($data['body'])) {
$payload = $data['body'];
} elseif (!empty($data['json'])) {
$payload = json_encode($data['json']);
}
$canonicalHeaders = '';
$signedHeaders = '';
if ($header) {
$canonicalHeadersAtrr = $signedHeadersAttr = [];
ksort($header);
foreach ($header as $key => $item) {
$key = strtolower($key);
if (isset(self::BLACKLIST_HEADERS[$key])) {
continue;
}
$canonicalHeadersAtrr[] = $key . ':' . preg_replace('/\s+/', ' ', trim($item));
$signedHeadersAttr[] = strtolower($key);
}
ksort($canonicalHeadersAtrr);
ksort($signedHeadersAttr);
if ($canonicalHeadersAtrr) {
$canonicalHeaders = implode("\n", $canonicalHeadersAtrr);
$signedHeaders = implode(';', $signedHeadersAttr);
}
}
$canonicalUri = $this->createCanonicalizedPath($url);
$bodyDigest = $this->buildBodyDigest($header, (string)Utils::streamFor($payload));
$canonicalRequest = $httpMethod . "\n" . $canonicalUri . "\n" . $canonicalQueryString . "\n" . $canonicalHeaders . "\n" . "\n" . $signedHeaders . "\n" . $bodyDigest;
return [$canonicalRequest, $signedHeaders];
}
/**
*
* @param string $url
* @return string
*/
public function createCanonicalizedPath(string $url)
{
$canonicalUri = '/';
$urlAttr = pathinfo($url);
if (isset($urlAttr['dirname']) && $urlAttr['dirname'] !== 'https:') {
$urlParse = parse_url($urlAttr['dirname'] ?? '');
if (isset($urlParse['path'])) {
$canonicalUri .= substr($urlParse['path'], 1) . '/';
}
if (isset($urlAttr['basename'])) {
$canonicalUri .= $urlAttr['basename'];
}
//|| strlen($canonicalUri) - 1 !== $pos
if (!($pos = strripos($canonicalUri, '/')) ) {
$canonicalUri .= '/';
}
}
return $canonicalUri;
}
/**
* 处理dody hash
* @param array $header
* @param string $body
* @return string
*/
protected function buildBodyDigest(array $header, string $body = ''): string
{
if (isset($header['x-amz-content-sha256'])) {
$hash = $header['x-amz-content-sha256'];
} else {
$hash = hash('sha256', $body);
}
return $hash;
}
/**
* 处理bogy
* @param string $body
* @return string
*/
public function dechunk(string $body): string
{
$h = fopen('php://temp', 'w+');
stream_filter_append($h, 'dechunk', \STREAM_FILTER_WRITE);
fwrite($h, $body);
$body = stream_get_contents($h, -1, 0);
rewind($h);
ftruncate($h, 0);
return $body;
}
/**
* 获取区域
* @return \string[][]
*/
public function getRegion()
{
return [
[
'value' => 'cn-north-1',
'label' => '华北-北京',
],
[
'value' => 'cn-south-1',
'label' => '华南-广州',
],
[
'value' => 'cn-east-2',
'label' => '华东-上海',
],
[
'value' => 'cn-east-1',
'label' => '华东-宿迁',
]
];
}
}

View File

@@ -0,0 +1,717 @@
<?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\upload\extend\obs;
use crmeb\exceptions\UploadException;
use crmeb\services\upload\BaseClient;
/**
* 华为云上传
* Class Client
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
* @package crmeb\services\upload\extend\obs
*/
class Client extends BaseClient
{
const HEADER_PREFIX = 'x-obs-';
const INTEREST_HEADER_KEY_LIST = ['content-type', 'content-md5', 'date'];
const ALTERNATIVE_DATE_HEADER = 'x-obs-date';
const ALLOWED_RESOURCE_PARAMTER_NAMES = [
'acl',
'policy',
'torrent',
'logging',
'location',
'storageinfo',
'quota',
'storagepolicy',
'requestpayment',
'versions',
'versioning',
'versionid',
'uploads',
'uploadid',
'partnumber',
'website',
'notification',
'lifecycle',
'deletebucket',
'delete',
'cors',
'restore',
'tagging',
'response-content-type',
'response-content-language',
'response-expires',
'response-cache-control',
'response-content-disposition',
'response-content-encoding',
'x-image-process',
'backtosource',
'storageclass',
'replication',
'append',
'position',
'x-oss-process'
];
//桶acl
const OBS_ACL = [
[
'value' => 'public-read',
'label' => '公共读(推荐)',
],
[
'value' => 'public-read-write',
'label' => '公共读写',
],
];
//默认acl
const DEFAULT_OBS_ACL = 'public-read';
protected $isCname = false;
protected $pathStyle;
/**
* @var
*/
protected $accessKeyId;
/**
* @var
*/
protected $secretKey;
/**
* 桶名
* @var string
*/
protected $bucketName;
/**
* 地区
* @var string
*/
protected $region;
/**
* @var mixed|string
*/
protected $uploadUrl;
/**
* @var string
*/
protected $baseUrl = 'obs.cn-north-1.myhuaweicloud.com';
protected $type = 'hw';
/**
* Client constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->accessKeyId = $config['accessKey'] ?? '';
$this->secretKey = $config['secretKey'] ?? '';
$this->bucketName = $config['bucket'] ?? '';
$this->region = $config['region'] ?? 'ap-chengdu';
$this->uploadUrl = $config['uploadUrl'] ?? '';
$this->type = $config['type'] ?? 'hw';
}
/**
* 检测
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
protected function checkOptions()
{
if (!$this->bucketName) {
throw new UploadException('请传入桶名');
}
if (!$this->region) {
throw new UploadException('请传入所属地域');
}
if (!$this->accessKeyId) {
throw new UploadException('请传入SecretId');
}
if (!$this->secretKey) {
throw new UploadException('请传入SecretKey');
}
return $this;
}
/**
* 上传图片
* @param string $key
* @param $body
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function putObject(string $key, $body, string $contentType = 'image/jpeg')
{
$header = [
'Host' => $this->getRequestUrl($this->bucketName, $this->region),
'Content-Type' => $contentType,
'Content-Length' => strlen($body),
];
$res = $this->checkOptions()->request('https://' . $header['Host'] . '/' . $key, 'PUT', [
'bucket' => $this->bucketName,
'body' => $body
], $header);
return $this->response($res);
}
/**
* 删除上传对象
* @param string $key
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function deleteObject(string $key)
{
$header = [
'Host' => $this->getRequestUrl($this->bucketName, $this->region),
];
$res = $this->request('https://' . $header['Host'] . '/' . $key, 'DELETE', [
'bucket' => $this->bucketName
], $header);
return $this->response($res);
}
/**
* 获取桶
* @return false|string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/16
*/
public function listBuckets()
{
$header = [
'Host' => $this->getRequestUrl('', $this->region),
];
$res = $this->request('https://' . $header['Host'] . '/', 'GET', [], []);
return $this->response($res);
}
public function headBucket(string $bucket, string $region)
{
$header = [
'Host' => $this->getRequestUrl($bucket, $region),
];
$res = $this->request('https://' . $header['Host'] . '/', 'HEAD', [], []);
return $this->response($res);
}
/**
* 设置桶的策略
* @param string $bucket
* @param string $region
* @param array $data
* @return mixed
*
* @date 2023/06/08
* @author yyw
*/
public function putPolicy(string $bucket, string $region, array $data)
{
$header = [
'Host' => $this->getRequestUrl($bucket, $region),
"Content-Type" => "application/json"
];
$res = $this->request('https://' . $header['Host'] . '/?policy', 'PUT', [
'bucket' => $bucket,
'json' => $data
], $header);
return $this->response($res);
}
/**
* 创建桶
* @param string $bucket
* @param string $region
* @param string $acl
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function createBucket(string $bucket, string $region, string $acl = self::DEFAULT_OBS_ACL)
{
$header = [
'x-obs-acl' => $acl,
'Host' => $this->getRequestUrl($bucket, $region),
"Content-Type" => "application/xml"
];
$xml = "<CreateBucketConfiguration><Location>{$region}</Location></CreateBucketConfiguration>";
$res = $this->request('https://' . $header['Host'] . '/', 'PUT', [
'bucket' => $bucket,
'body' => $xml
], $header);
return $this->response($res);
}
/**
* 删除桶
* @param string $bucket
* @param string $region
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function deleteBucket(string $bucket, string $region)
{
$header = [
'Host' => $this->getRequestUrl($bucket, $region),
];
$res = $this->request('https://' . $header['Host'] . '/', 'DELETE', [
'bucket' => $bucket
], $header);
return $this->response($res);
}
/**
* 获取桶的自定义域名
* @param string $bucket
* @param string $region
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function getBucketDomain(string $bucket, string $region)
{
$header = [
'Host' => $this->getRequestUrl($bucket, $region),
];
$res = $this->request('https://' . $header['Host'] . '/?customdomain', 'GET', [
'bucket' => $bucket
], $header);
return $this->response($res);
}
/**
* 设置桶的自定义域名
* @param string $bucket
* @param string $region
* @param array $data
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function putBucketDomain(string $bucket, string $region, array $data = [])
{
$header = [
'Host' => $this->getRequestUrl($bucket, $region),
];
$res = $this->request('https://' . $header['Host'] . '/?customdomain=' . $data['domainname'], 'PUT', [
'bucket' => $bucket
], $header);
return $this->response($res);
}
/**
* 设置跨域
* @return bool
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
public function putBucketCors(string $bucket, string $region, array $data = [])
{
$xml = $this->xmlBuild($data, 'CORSConfiguration', 'CORSRule');
$header = [
'Host' => $this->getRequestUrl($bucket, $region),
'Content-Type' => 'application/xml',
'Content-Length' => strlen($xml),
'Content-MD5' => base64_encode(md5($xml, true))
];
$res = $this->request('https://' . $header['Host'] . '/?cors', 'PUT', [
'bucket' => $bucket,
'body' => $xml
], $header);
return $this->response($res);
}
/**
* 删除跨域
* @param string $bucket
* @param string $region
* @return mixed
*
* @date 2023/06/08
* @author yyw
*/
public function deleteBucketCors(string $bucket, string $region)
{
$header = [
'Host' => $this->getRequestUrl($bucket, $region),
];
$res = $this->request('https://' . $header['Host'] . '/?cors', 'DELETE', [
'bucket' => $bucket,
], $header);
return $this->response($res);
}
/**
* @param $res
* @return mixed
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/18
*/
protected function response($res)
{
if (!empty($res['Code']) && !empty($res['Message'])) {
throw new UploadException($res['Message']);
}
return $res;
}
/**
* 获取请求域名
* @param string $bucket
* @param string $region
* @return string
*
* @date 2023/06/08
* @author yyw
*/
protected function getRequestUrl(string $bucket = '', string $region = '')
{
if ($this->type == 'hw') {
$url = '.myhuaweicloud.com'; // 华为
} else {
$url = '.ctyun.cn'; // 天翼
}
if ($bucket) {
return $bucket . '.obs.' . $region . $url;
} else {
return 'obs.' . $region . $url;
}
}
/**
* 地域名称
* @return \string[][]
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/17
*/
public function getRegion()
{
return [
[
'value' => 'cn-north-1',
'label' => '华北-北京一',
],
// [
// 'value' => 'cn-north-4',
// 'label' => '华北-北京四',
// ],
[
'value' => 'cn-north-9',
'label' => '华北-乌兰察布一',
],
[
'value' => 'cn-east-2',
'label' => '华东-上海二',
],
[
'value' => 'cn-east-3',
'label' => '华东-上海一',
],
[
'value' => 'cn-south-1',
'label' => '华南-广州',
],
[
'value' => 'ap-southeast-1',
'label' => '中国-香港',
],
[
'value' => 'cn-south-4',
'label' => '华南-广州-友好用户环境',
],
[
'value' => 'cn-southwest-2',
'label' => '西南-贵阳一',
],
[
'value' => 'la-north-2',
'label' => '拉美-墨西哥城二',
],
[
'value' => 'na-mexico-1',
'label' => '拉美-墨西哥城一',
],
[
'value' => 'sa-brazil-1',
'label' => '拉美-圣保罗一',
],
[
'value' => 'la-south-2',
'label' => '拉美-圣地亚哥',
],
[
'value' => 'tr-west-1',
'label' => '土耳其-伊斯坦布尔',
],
[
'value' => 'ap-southeast-2',
'label' => '亚太-曼谷',
],
[
'value' => 'ap-southeast-3',
'label' => '亚太-新加坡',
],
[
'value' => 'af-south-1',
'label' => '非洲-约翰内斯堡',
]
];
}
/**
* 设置桶名
* @param string $bucketName
* @return $this
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/16
*/
public function setBucketName(string $bucketName)
{
$this->bucketName = $bucketName;
return $this;
}
/**
* 获取签名
* @param array $result
* @return array
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/17
*/
protected function getSign(array $result)
{
$result['headers']['Date'] = gmdate('D, d M Y H:i:s \G\M\T');
$canonicalstring = $this->makeCanonicalstring($result['method'], $result['headers'], $result['pathArgs'], $result['dnsParam'], $result['uriParam']);
$result['cannonicalRequest'] = $canonicalstring;
$signature = base64_encode(hash_hmac('sha1', $canonicalstring, $this->secretKey, true));
$authorization = 'OBS ' . $this->accessKeyId . ':' . $signature;
$result['headers']['Authorization'] = $authorization;
return $result;
}
/**
* 处理签名数据
* @param $method
* @param $headers
* @param $pathArgs
* @param $bucketName
* @param $objectKey
* @param null $expires
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/17
*/
public function makeCanonicalstring($method, $headers, $pathArgs, $bucketName, $objectKey, $expires = null)
{
$buffer = [];
$buffer[] = $method;
$buffer[] = "\n";
$interestHeaders = [];
foreach ($headers as $key => $value) {
$key = strtolower($key);
if (in_array($key, self::INTEREST_HEADER_KEY_LIST) || strpos($key, self::HEADER_PREFIX) === 0) {
$interestHeaders[$key] = $value;
}
}
if (array_key_exists(self::ALTERNATIVE_DATE_HEADER, $interestHeaders)) {
$interestHeaders['date'] = '';
}
if ($expires !== null) {
$interestHeaders['date'] = strval($expires);
}
if (!array_key_exists('content-type', $interestHeaders)) {
$interestHeaders['content-type'] = '';
}
if (!array_key_exists('content-md5', $interestHeaders)) {
$interestHeaders['content-md5'] = '';
}
ksort($interestHeaders);
foreach ($interestHeaders as $key => $value) {
if (strpos($key, self::HEADER_PREFIX) === 0) {
$buffer[] = $key . ':' . $value;
} else {
$buffer[] = $value;
}
$buffer[] = "\n";
}
$uri = '';
$bucketName = $this->isCname ? $headers['Host'] : $bucketName;
if ($bucketName) {
$uri .= '/';
$uri .= $bucketName;
if (!$this->pathStyle) {
$uri .= '/';
}
}
if ($objectKey) {
if (!($pos = strripos($uri, '/')) || strlen($uri) - 1 !== $pos) {
$uri .= '/';
}
$uri .= $objectKey;
}
$buffer[] = $uri === '' ? '/' : $uri;
if (!empty($pathArgs)) {
ksort($pathArgs);
$_pathArgs = [];
foreach ($pathArgs as $key => $value) {
if (in_array(strtolower($key), self::ALLOWED_RESOURCE_PARAMTER_NAMES) || strpos($key, self::HEADER_PREFIX) === 0) {
$_pathArgs[] = $value === null || $value === '' ? $key : $key . '=' . urldecode($value);
}
}
if (!empty($_pathArgs)) {
$buffer[] = '?';
$buffer[] = implode('&', $_pathArgs);
}
}
return implode('', $buffer);
}
/**
* 发起请求
* @param string $url
* @param string $method
* @param array $data
* @param array $clientHeader
* @param int $timeout
* @return false|string
* @author 等风来
* @email 136327134@qq.com
* @date 2023/5/16
*/
public function request(string $url, string $method, array $data = [], array $clientHeader = [], int $timeout = 10)
{
$method = strtoupper($method);
$urlAttr = pathinfo($url);
$urlParse = parse_url($urlAttr['dirname'] ?? '');
$uriParam = '';
if ($urlAttr['dirname'] !== 'https:') {
if (isset($urlParse['path'])) {
$uriParam .= substr($urlParse['path'], 1) . '/';
}
if (isset($urlAttr['basename'])) {
$uriParam .= $urlAttr['basename'];
}
}
$result = $this->getSign([
'method' => $method,
'headers' => $clientHeader,
'pathArgs' => '',
'dnsParam' => $data['bucket'] ?? '',
'uriParam' => $uriParam,
]);
return $this->requestClient($url, $method, $data, $result['headers'], $timeout);
}
/**
* 组合成xml
* @param array $data
* @param string $root
* @param string $itemKey
* @return string
* @author 等风来
* @email 136327134@qq.com
* @date 2022/10/17
*/
protected function xmlBuild(array $xmlAttr, string $root = 'xml', string $itemKey = 'item')
{
$xml = '<' . $root . '>';
$xml .= '<' . $itemKey . '>';
foreach ($xmlAttr as $kk => $vv) {
if (is_array($vv)) {
foreach ($vv as $v) {
$xml .= '<' . $kk . '>' . $v . '</' . $kk . '>';
}
} else {
$xml .= '<' . $kk . '>' . $vv . '</' . $kk . '>';
}
}
$xml .= '</' . $itemKey . '>';
$xml .= '</' . $root . '>';
return $xml;
}
}

View File

@@ -0,0 +1,676 @@
<?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\upload\storage;
use crmeb\basic\BaseUpload;
use crmeb\exceptions\UploadException;
use GuzzleHttp\Psr7\Utils;
use think\exception\ValidateException;
use QCloud\COSSTS\Sts;
use crmeb\services\upload\extend\cos\Client as CrmebClient;
/**
* 腾讯云COS文件上传
* Class COS
* @package crmeb\services\upload\storage
*/
class Cos extends BaseUpload
{
/**
* 应用id
* @var string
*/
protected $appid;
/**
* accessKey
* @var mixed
*/
protected $accessKey;
/**
* secretKey
* @var mixed
*/
protected $secretKey;
/**
* 句柄
* @var CrmebClient
*/
protected $handle;
/**
* 空间域名 Domain
* @var mixed
*/
protected $uploadUrl;
/**
* 存储空间名称 公开空间
* @var mixed
*/
protected $storageName;
/**
* COS使用 所属地域
* @var mixed|null
*/
protected $storageRegion;
/**
* 水印位置
* @var string[]
*/
protected $position = [
'1' => 'northwest',//:左上
'2' => 'north',//:中上
'3' => 'northeast',//:右上
'4' => 'west',//:左中
'5' => 'center',//:中部
'6' => 'east',//:右中
'7' => 'southwest',//:左下
'8' => 'south',//:中下
'9' => 'southeast',//:右下
];
/**
* 初始化
* @param array $config
* @return mixed|void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->accessKey = $config['accessKey'] ?? null;
$this->secretKey = $config['secretKey'] ?? null;
$this->appid = $config['appid'] ?? null;
$this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '');
$this->storageName = $config['storageName'] ?? null;
$this->storageRegion = $config['storageRegion'] ?? null;
$this->waterConfig['watermark_text_font'] = 'simfang仿宋.ttf';
}
/**
*
* @return CrmebClient
* @author 等风来
* @email 136327134@qq.com
* @date 2022/9/29
*/
protected function app()
{
$this->handle = new CrmebClient([
'accessKey' => $this->accessKey,
'secretKey' => $this->secretKey,
'region' => $this->storageRegion ?: 'ap-chengdu',
'bucket' => $this->storageName,
'appid' => $this->appid,
'uploadUrl' => $this->uploadUrl
]);
return $this->handle;
}
/**
* 上传文件
* @param string|null $file
* @param bool $isStream 是否为流上传
* @param string|null $fileContent 流内容
* @return array|bool|\StdClass
*/
protected function upload(string $file = null, bool $isStream = false, string $fileContent = null)
{
if (!$isStream) {
$fileHandle = app()->request->file($file);
if (!$fileHandle) {
return $this->setError('Upload file does not exist');
}
if ($this->validate) {
try {
$error = [
$file . '.filesize' => 'Upload filesize error',
$file . '.fileExt' => 'Upload fileExt error',
$file . '.fileMime' => 'Upload fileMine error'
];
validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
} catch (ValidateException $e) {
return $this->setError($e->getMessage());
}
}
$key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
$body = fopen($fileHandle->getRealPath(), 'rb');
$body = (string)Utils::streamFor($body);
} else {
$key = $file;
$body = $fileContent;
}
try {
$key = $this->getUploadPath($key);
$this->verifyExtension($key);
$this->fileInfo->uploadInfo = $this->app()->putObject($key, $body);
$this->fileInfo->filePath = $this->uploadUrl . '/' . $key;
$this->fileInfo->realName = isset($fileHandle) ? $fileHandle->getOriginalName() : $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (UploadException $e) {
return $this->setError($e->getMessage());
}
}
/**
* 文件流上传
* @param string $fileContent
* @param string|null $key
* @return array|bool|mixed|\StdClass
*/
public function stream(string $fileContent, string $key = null)
{
if (!$key) {
$key = $this->saveFileName();
}
return $this->upload($key, true, $fileContent);
}
/**
* 文件上传
* @param string $file
* @return array|bool|mixed|\StdClass
*/
public function move(string $file = 'file')
{
return $this->upload($file);
}
/**
* 缩略图
* @param string $filePath
* @param string $type
* @return mixed|string[]
*/
public function thumb(string $filePath = '', string $type = 'all')
{
$filePath = $this->getFilePath($filePath);
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
$config = $this->thumbConfig;
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
if (isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$key = 'filePath' . ucfirst($v);
$this->fileInfo->$key = $filePath . '?imageMogr2/thumbnail/' . $config[$width] . 'x' . $config[$height];
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
}
}
}
}
return $data;
}
/**
* 水印
* @param string $filePath
* @return mixed|string
*/
public function water(string $filePath = '')
{
$filePath = $this->getFilePath($filePath);
$waterConfig = $this->waterConfig;
$waterPath = $filePath;
if ($waterConfig['image_watermark_status'] && $filePath) {
if (strpos($filePath, '?') === false) {
$filePath .= '?watermark';
} else {
$filePath .= '&watermark';
}
switch ($waterConfig['watermark_type']) {
case 1://图片
if (!$waterConfig['watermark_image']) {
throw new ValidateException('请先配置水印图片');
}
$waterPath = $filePath .= '/1/image/' . base64_encode($waterConfig['watermark_image']) . '/gravity/' . ($this->position[$waterConfig['watermark_position']] ?? 'northwest') . '/blogo/1/dx/' . $waterConfig['watermark_x'] . '/dy/' . $waterConfig['watermark_y'];
break;
case 2://文字
if (!$waterConfig['watermark_text']) {
throw new ValidateException('请先配置水印文字');
}
$waterPath = $filePath .= '/2/text/' . base64_encode($waterConfig['watermark_text']) . '/font/' . base64_encode($waterConfig['watermark_text_font']) . '/fill/' . base64_encode($waterConfig['watermark_text_color']) . '/fontsize/' . $waterConfig['watermark_text_size'] . '/gravity/' . ($this->position[$waterConfig['watermark_position']] ?? 'northwest') . '/dx/' . $waterConfig['watermark_x'] . '/dy/' . $waterConfig['watermark_y'];
break;
}
}
return $waterPath;
}
/**
* 获取视频封面图
* @param string $filePath
* @param string $type
* @param int $time
* @return array
*/
public function videoCoverImage(string $filePath = '', string $type = 'all', int $time = 1)
{
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
//?ci-process=snapshot
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 600;
$width = 400;
$key = 'filePath' . ucfirst($v);
$this->fileInfo->$key = $filePath . '?ci-process=snapshot&time=' . $time . '&format=jpg&width=' . $width . '&height=' . $height ;
$data[$v] = $this->fileInfo->$key;
}
}
}
return $data;
}
/**
* 删除资源
* @param $key
* @return mixed
*/
public function delete(string $filePath)
{
try {
return $this->app()->deleteObject($this->storageName, $filePath);
} catch (\Exception $e) {
return $this->setError($e->getMessage());
}
}
/**
* 生成签名
* @return array|mixed
* @throws \Exception
*/
public function getTempKeys()
{
$sts = new Sts();
$config = [
'url' => 'https://sts.tencentcloudapi.com/',
'domain' => 'sts.tencentcloudapi.com',
'proxy' => '',
'secretId' => $this->accessKey, // 固定密钥
'secretKey' => $this->secretKey, // 固定密钥
'bucket' => $this->storageName, // 换成你的 bucket
'region' => $this->storageRegion, // 换成 bucket 所在园区
'durationSeconds' => 1800, // 密钥有效期
'allowPrefix' => '*', // 这里改成允许的路径前缀,可以根据自己网站的用户登录态判断允许上传的具体路径,例子: a.jpg 或者 a/* 或者 * (使用通配符*存在重大安全风险, 请谨慎评估使用)
// 密钥的权限列表。简单上传和分片需要以下的权限,其他权限列表请看 https://cloud.tencent.com/document/product/436/31923
'allowActions' => [
// 简单上传
'name/cos:PutObject',
'name/cos:PostObject',
// 分片上传
'name/cos:InitiateMultipartUpload',
'name/cos:ListMultipartUploads',
'name/cos:ListParts',
'name/cos:UploadPart',
'name/cos:CompleteMultipartUpload'
]
];
// 获取临时密钥,计算签名
$result = $sts->getTempKeys($config);
$result['url'] = $this->uploadUrl . '/';
$result['type'] = 'COS';
$result['bucket'] = $this->storageName;
$result['region'] = $this->storageRegion;
return $result;
}
/**
* 计算临时密钥用的签名
* @param $opt
* @param $key
* @param $method
* @param $config
* @return string
*/
public function getSignature($opt, $key, $method, $config)
{
$formatString = $method . $config['domain'] . '/?' . $this->json2str($opt, 1);
$sign = hash_hmac('sha1', $formatString, $key);
$sign = base64_encode($this->_hex2bin($sign));
return $sign;
}
public function _hex2bin($data)
{
$len = strlen($data);
return pack("H" . $len, $data);
}
// obj 转 query string
public function json2str($obj, $notEncode = false)
{
ksort($obj);
$arr = array();
if (!is_array($obj)) {
return $this->setError($obj . " must be a array");
}
foreach ($obj as $key => $val) {
array_push($arr, $key . '=' . ($notEncode ? $val : rawurlencode($val)));
}
return join('&', $arr);
}
// v2接口的key首字母小写,v3改成大写,此处做了向下兼容
public function backwardCompat($result)
{
if (!is_array($result)) {
return $this->setError($result . " must be a array");
}
$compat = array();
foreach ($result as $key => $value) {
if (is_array($value)) {
$compat[lcfirst($key)] = $this->backwardCompat($value);
} elseif ($key == 'Token') {
$compat['sessionToken'] = $value;
} else {
$compat[lcfirst($key)] = $value;
}
}
return $compat;
}
/**
* 桶列表
* @param string|null $region
* @param bool $line
* @param bool $shared
* @return array|mixed
* "Name" => "record-1254950941"
* "Location" => "ap-chengdu"
* "CreationDate" => "2019-05-16T08:33:29Z"
* "BucketType" => "cos"
*/
public function listbuckets(string $region = null, bool $line = false, bool $shared = false)
{
try {
$res = $this->app()->listBuckets();
$bucket = [];
if (isset($res['Buckets'][0]['Bucket'])) {
if (isset($res['Buckets'][0]['Bucket']['Name'])) {
$bucket[] = $res['Buckets'][0]['Bucket'];
} else {
$bucket = $res['Buckets'][0]['Bucket'];
}
} else if (isset($res['Buckets']['Bucket'])) {
$bucket = $res['Buckets']['Bucket'];
}
return $bucket;
} catch (\Throwable $e) {
return [];
}
}
/**
* 创建桶
* @param string $name
* @param string $region
* @param string $acl public-read=公共独写
* @return bool|mixed
*/
public function createBucket(string $name, string $region = '', string $acl = 'public-read')
{
$regionData = $this->getRegion();
$regionData = array_column($regionData, 'value');
if (!in_array($region, $regionData)) {
return $this->setError('COS:无效的区域!');
}
$this->storageRegion = $region;
$app = $this->app();
//检测桶
try {
$res1 = $app->headBucket($name, $region);
if ($res1 !== true) {
return $this->setError('COS:设置的桶名已经存在!');
}
} catch (\Throwable $e) {
//桶不存在返回404
if (strstr('404', $e->getMessage())) {
return $this->setError('COS:' . $e->getMessage());
}
}
//创建桶
try {
$res = $app->createBucket($name, $region, $acl);
} catch (\Throwable $e) {
if (strstr('[curl] 6', $e->getMessage())) {
return $this->setError('COS:无效的区域!!');
} else if (strstr('Access Denied.', $e->getMessage())) {
return $this->setError('COS:无权访问');
}
return $this->setError('COS:' . $e->getMessage());
}
return $res;
}
/**
* 删除桶
* @param string $name
* @return bool|mixed
*/
public function deleteBucket(string $name)
{
try {
$this->app()->deleteBucket($name);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
return true;
}
/**
* @param string $name
* @param string|null $region
* @return array|object
*/
public function getDomian(string $name, string $region = null)
{
try {
$res = $this->app()->getBucketDomain($name, $region);
if (isset($res['DomainRule'])) {
$domainRules[] = $res['DomainRule']['Name'];
} else {
$domainRules = array_column($res['DomainRules'], 'Name');
}
return $domainRules;
} catch (\Throwable $e) {
}
return [];
}
/**
* 绑定域名
* @param string $name
* @param string $domain
* @param string|null $region
* @return bool|mixed
*/
public function bindDomian(string $name, string $domain, string $region = null)
{
$this->storageRegion = $region;
$parseDomin = parse_url($domain);
try {
$this->app()->putBucketDomain($name, $region, [
'Name' => $parseDomin['host'],
'Status' => 'ENABLED',
'Type' => 'REST',
'ForcedReplacement' => 'CNAME'
]);
return true;
} catch (\Throwable $e) {
if ($message = $this->setMessage($e->getMessage())) {
return $this->setError($message);
}
return $this->setError($e->getMessage());
}
return false;
}
/**
* 处理
* @param string $message
* @return string
*/
protected function setMessage(string $message)
{
$data = [
'The specified bucket does not exist.' => '指定的存储桶不存在。',
'Please add CNAME/TXT record to DNS then try again later. Please allow up to 10 mins before your DNS takes effect.' => '请将CNAME记录添加到DNS,然后稍后重试。在DNS生效前,请等待最多10分钟。'
];
$msg = $data[$message] ?? '';
if ($msg) {
return $msg;
}
foreach ($data as $item) {
if (strstr($message, $item)) {
return $item;
}
}
return '';
}
/**
* 设置跨域
* @param string $name
* @param string $region
* @return bool
*/
public function setBucketCors(string $name, string $region)
{
$this->storageRegion = $region;
try {
$this->app()->putBucketCors($name, [
'AllowedHeader' => ['*'],
'AllowedMethod' => ['PUT', 'GET', 'POST', 'DELETE', 'HEAD'],
'AllowedOrigin' => ['*'],
'ExposeHeader' => ['ETag', 'Content-Length', 'x-cos-request-id'],
'MaxAgeSeconds' => 12
], $region);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
return true;
}
/**
* 地域
* @return mixed|\string[][]
*/
public function getRegion()
{
return [
[
'value' => 'ap-chengdu',
'label' => '成都'
],
[
'value' => 'ap-shanghai',
'label' => '上海'
],
[
'value' => 'ap-guangzhou',
'label' => '广州'
],
[
'value' => 'ap-nanjing',
'label' => '南京'
],
[
'value' => 'ap-beijing',
'label' => '北京'
],
[
'value' => 'ap-chongqing',
'label' => '重庆'
],
[
'value' => 'ap-shenzhen-fsi',
'label' => '深圳金融'
],
[
'value' => 'ap-shanghai-fsi',
'label' => '上海金融'
],
[
'value' => 'ap-beijing-fsi',
'label' => '北京金融'
],
[
'value' => 'ap-hongkong',
'label' => '中国香港'
],
[
'value' => 'ap-singapore',
'label' => '新加坡'
],
[
'value' => 'ap-mumbai',
'label' => '孟买'
],
[
'value' => 'ap-jakarta',
'label' => '雅加达'
],
[
'value' => 'ap-seoul',
'label' => '首尔'
],
[
'value' => 'ap-bangkok',
'label' => '曼谷'
],
[
'value' => 'ap-tokyo',
'label' => '东京'
],
[
'value' => 'na-siliconvalley',
'label' => '硅谷(美西)'
],
[
'value' => 'na-ashburn',
'label' => '弗吉尼亚(美东)'
],
[
'value' => 'na-toronto',
'label' => '多伦多'
],
[
'value' => 'sa-saopaulo',
'label' => '圣保罗'
],
[
'value' => 'eu-frankfurt',
'label' => '法兰克福'
],
[
'value' => 'eu-moscow',
'label' => '莫斯科'
]
];
}
}

View File

@@ -0,0 +1,527 @@
<?php
namespace crmeb\services\upload\storage;
use Aws\Acm\Exception\AcmException;
use crmeb\exceptions\AdminException;
use crmeb\exceptions\UploadException;
use crmeb\basic\BaseUpload;
use GuzzleHttp\Psr7\Utils;
use think\exception\ValidateException;
/**
* 京东云COS文件上传
* Class Jdoss
* @package crmeb\services\upload\storage
*/
class Jdoss extends BaseUpload
{
/**
* 应用id
* @var string
*/
protected $appid;
/**
* accessKey
* @var mixed
*/
protected $accessKey;
/**
* secretKey
* @var mixed
*/
protected $secretKey;
/**
* 句柄
* @var S3Client
*/
protected $handle;
/**
* 空间域名 Domain
* @var mixed
*/
protected $uploadUrl;
/**
* 存储空间名称 公开空间
* @var mixed
*/
protected $storageName;
/**
* COS使用 所属地域
* @var mixed|null
*/
protected $storageRegion;
/**
* @var string
*/
protected $cdn;
/**
* 水印位置
* @var string[]
*/
protected $position = [
'1' => '1',//:左上
'2' => '2',//:中上
'3' => '3',//:右上
'4' => '4',//:左中
'5' => '5',//:中部
'6' => '6',//:右中
'7' => '7',//:左下
'8' => '8',//:中下
'9' => '9',//:右下
];
/**
* 初始化
* @param array $config
* @return mixed|void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->accessKey = $config['accessKey'] ?? null;
$this->secretKey = $config['secretKey'] ?? null;
$this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '') ?: sys_config('site_url');
$this->storageName = $config['storageName'] ?? null;
$this->storageRegion = $config['storageRegion'] ?? null;
$this->waterConfig['watermark_text_font'] = 'simfang仿宋.ttf';
}
/**
* @return \crmeb\services\upload\extend\jdoss\Client
*/
protected function app()
{
if (!$this->accessKey || !$this->secretKey) {
throw new UploadException('Please configure accessKey and secretKey');
}
$this->handle = new \crmeb\services\upload\extend\jdoss\Client([
'accessKey' => $this->accessKey,
'secretKey' => $this->secretKey,
]);
return $this->handle;
}
/**
* 上传图片
* @param string $file
* @return array|bool|mixed
*/
public function move(string $file = 'file')
{
$fileHandle = app()->request->file($file);
if (!$fileHandle) {
return $this->setError('上传的文件不存在');
}
if ($this->validate) {
try {
$error = [
$file . '.filesize' => 'Upload filesize error',
$file . '.fileExt' => 'Upload fileExt error',
$file . '.fileMime' => 'Upload fileMine error'
];
validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
} catch (ValidateException $e) {
return $this->setError($e->getMessage());
}
}
$key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
$key = $this->getUploadPath($key);
$body = fopen($fileHandle->getRealPath(), 'rb');
$body = (string)Utils::streamFor($body);
try {
$uploadInfo = $this->app()->putObject($this->storageName, $this->storageRegion, $key, [
'body' => $body
]);
if (!$uploadInfo) {
return $this->setError('Upload failure');
}
$this->fileInfo->uploadInfo = $uploadInfo;
$this->fileInfo->realName = $fileHandle->getOriginalName();
$this->fileInfo->filePath = ($this->cdn ?: $this->uploadUrl) . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 文件流上传
* @param string $fileContent
* @param string|null $key
* @return bool|mixed
*/
public function stream(string $fileContent, string $key = null)
{
try {
if (!$key) {
$key = $this->saveFileName();
}
$key = $this->getUploadPath($key);
$fileContent = (string)Utils::streamFor($fileContent);
$uploadInfo = $this->app()->putObject($this->storageName, $this->storageRegion, $key, [
'body' => $fileContent
]);
if (!$uploadInfo) {
return $this->setError('Upload failure');
}
$this->fileInfo->uploadInfo = $uploadInfo;
$this->fileInfo->realName = $key;
$this->fileInfo->filePath = ($this->cdn ?: $this->uploadUrl) . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 删除
* @param string $key
* @return array|bool|\crmeb\services\upload\extend\cos\SimpleXMLElement|mixed
*/
public function delete(string $key)
{
try {
return $this->app()->deleteObject($this->storageName, $this->storageRegion, $key);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* @param string $region
* @param bool $line
* @param bool $shared
* @return array|\Aws\Result
*/
public function listbuckets(string $region = 'cn-north-1', bool $line = false, bool $shared = false)
{
try {
$res = $this->app()->listBuckets();
return $res['Buckets']['Bucket'] ?? [];
} catch (\Throwable $e) {
return [];
}
}
/**
* 创建桶
* @param string $name
* @param string $region
* @param string $acl
* @return array|\AsyncAws\S3\Result\CreateBucketOutput|bool|\crmeb\services\upload\extend\cos\SimpleXMLElement
*/
public function createBucket(string $name, string $region = '', string $acl = 'public-read')
{
$regionData = $this->getRegion();
$regionData = array_column($regionData, 'value');
if (!in_array($region, $regionData)) {
return $this->setError('COS:无效的区域!');
}
$this->storageRegion = $region;
$app = $this->app();
//检测桶
try {
$app->headBucket($name, $region);
} catch (\Throwable $e) {
//桶不存在返回404
if (strstr('404', $e->getMessage())) {
return $this->setError('JDOSS:' . $e->getMessage());
}
}
//创建桶
try {
$res = $app->createBucket($name, $region, $acl);
} catch (\Throwable $e) {
return $this->setError('JDOSS:' . $e->getMessage());
}
return $res;
}
/**
* 删除桶
* @param string $name
* @param string $region
* @return bool
*/
public function deleteBucket(string $name, string $region = '')
{
try {
$this->storageRegion = $region;
$this->app()->deleteBucket($name, $region);
return true;
} catch (AcmException $e) {
return $this->setError($e->getMessage());
}
}
public function getRegion()
{
return [
[
'value' => 'cn-north-1',
'label' => '华北-北京'
],
[
'value' => 'cn-east-1',
'label' => '华东-宿迁'
],
[
'value' => 'cn-east-2',
'label' => '华东-上海'
],
[
'value' => 'cn-south-1',
'label' => '华南-广州'
]
];
}
public function getDomian(string $name, string $region = null)
{
try {
$this->storageRegion = $region;
$res = $this->app()->getBucketPolicy([
'Bucket' => $name
]);
return $res['DomainName'] ?? [];
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
public function bindDomian(string $name, string $domain, string $region = null)
{
try {
$this->storageRegion = $region;
$this->app()->putBucketWebsite([
'Bucket' => $name,
'WebsiteConfiguration' => [
'RedirectAllRequestsTo' => [
'HostName' => $domain,
'Protocol' => 'http'
]
]
]);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
public function setBucketCors(string $name, string $region)
{
$this->storageRegion = $region;
try {
$this->app()->putBucketCors($name, $region,
['CORSConfiguration' => [ // REQUIRED
'CORSRules' => [ // REQUIRED
[
'AllowedHeaders' => ['*'],
'AllowedMethods' => ['POST', 'GET', 'PUT', 'DELETE', 'HEAD'], // REQUIRED
'AllowedOrigins' => ['*'], // REQUIRED
'ExposeHeaders' => ['Etag'],
'MaxAgeSeconds' => 0
],
],
]
]);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 获取OSS上传密钥
* @return mixed|void
*/
public function getTempKeys($key = '', $path = '', $contentType = '', $expires = '+10 minutes')
{
try {
$app = $this->app();
$host = $app->getRequestUrl() . '/' . $this->storageName;
$url = 'https://' . $host . '/' . $key;
$params = $this->getTempKeysParam($host, $url);
$param = [];
foreach ($params as $k => $value) {
$param[] = $k . '=' . $value;
}
return [
'upload_url' => $url . '?' . implode('&', $param),
'type' => 'JDOSS',
'url' => $this->uploadUrl . '/' . $key
];
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 获取上传需要参数
* @param string $host
* @return array
*/
public function getTempKeysParam(string $host, string $url)
{
$amzDate = gmdate('Ymd\THis\Z');
$sdt = substr($amzDate, 0, 8);
$credentialScope = $sdt . '/' . $this->storageRegion . '/' . 's3' . '/aws4_request';
$clientHeader = [
'Host' => $host,
'X-Amz-Content-Sha256' => 'UNSIGNED-PAYLOAD',
'X-Amz-Date' => $amzDate
];
$param = [
'X-Amz-Content-Sha256' => 'UNSIGNED-PAYLOAD',
'X-Amz-Algorithm' => 'AWS4-HMAC-SHA256',
'X-Amz-Credential' => $this->accessKey . '/' . $credentialScope,
'X-Amz-Date' => $amzDate,
'X-Amz-SignedHeaders' => 'host;X-Amz-Content-Sha25;X-Amz-Date',
'X-Amz-Expires' => 600
];
[$canonicalRequest, $signedHeaders] = $this->app()->createCanonicalRequest($url, 'GET', $clientHeader, ['query' => $param]);
$stringToSign = "AWS4-HMAC-SHA256\n" . $amzDate . "\n" . $credentialScope . "\n" . hash('sha256', $canonicalRequest);
$key = $this->getSigningKey($sdt, $this->storageRegion, 's3', $this->accessKey);
$signature = hash_hmac('sha256', $stringToSign, $key);
$param['X-Amz-Signature'] = $signature;
return $param;
}
/**
* 生成key
* @param $shortDate
* @param $region
* @param $service
* @param $secretKey
* @return false|string
*/
public function getSigningKey($shortDate, $region, $service, $secretKey)
{
$kSecret = 'AWS4' . $secretKey;
$kDate = hash_hmac('sha256', $shortDate, $kSecret, true);
$kRegion = hash_hmac('sha256', $region, $kDate, true);
$kService = hash_hmac('sha256', $service, $kRegion, true);
return hash_hmac('sha256', 'aws4_request', $kService, true);
}
/**
* 缩略图
* @param string $filePath
* @param string $fileName
* @param string $type
* @return array|mixed
*/
public function thumb(string $filePath = '', string $fileName = '', string $type = 'all')
{
$filePath = $this->getFilePath($filePath);
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
$config = $this->thumbConfig;
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
$key = 'filePath' . ucfirst($v);
//x-oss-process=img/s/200/300
if (sys_config('image_thumbnail_status', 1) && isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$this->fileInfo->$key = $filePath . '?x-oss-process=img/s' . $config[$width] . '/' . $config[$height];
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
} else {
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
}
}
}
}
return $data;
}
/**
* 水印
* @param string $filePath
* @return mixed|string
*/
public function water(string $filePath = '')
{
$filePath = $this->getFilePath($filePath);
$waterConfig = $this->waterConfig;
$waterPath = $filePath;
if ($waterConfig['image_watermark_status'] && $filePath) {
if (strpos($filePath, '?x-oss-process') === false) {
$filePath .= '?x-oss-process=img';
}
switch ($waterConfig['watermark_type']) {
case 1://图片
if (!$waterConfig['watermark_image']) {
throw new AdminException('请先配置水印图片');
}
//x-oss-process=img/wmi/wk/ZG93bmxvYWRzOmxvZ28ucG5n/ws/100
$waterPath = $filePath .= '/wmi/wk/' . base64_encode($waterConfig['watermark_image']) . '/wd/' . $waterConfig['watermark_opacity'] . '/wp/' . ($this->position[$waterConfig['watermark_position']] ?? '1') . '/wdx/' . $waterConfig['watermark_x'] . '/wdy/' . $waterConfig['watermark_y'];
break;
case 2://文字
if (!$waterConfig['watermark_text']) {
throw new AdminException('请先配置水印文字');
}
//?x-oss-process=img/wmt/wt/5Lqs5Lic5LqR
$waterConfig['watermark_text_color'] = str_replace('#', '', $waterConfig['watermark_text_color']);
$waterPath = $filePath .= '/wmt/wt/' . base64_encode($waterConfig['watermark_text']) . '/wc/' . $waterConfig['watermark_text_color'] . '/ws/' . $waterConfig['watermark_text_size'] . '/wp/' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . '/wdx/' . $waterConfig['watermark_x'] . '/wdy/' . $waterConfig['watermark_y'] . '/wr/' . $waterConfig['watermark_text_angle'];
break;
}
}
return $waterPath;
}
/**
* 获取视频封面图
* @param string $filePath
* @param string $type
* @param int $time
* @return array
*/
public function videoCoverImage(string $filePath = '', string $type = 'all', int $time = 1)
{
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
//?x-oss-process=video/snapshot,t_7000,f_jpg,w_800,h_600,m_fast
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 600;
$width = 400;
$key = 'filePath' . ucfirst($v);
$this->fileInfo->$key = $filePath . '?x-oss-process=video/snapshot,t_' . ($time * 1000) . ',f_jpg,w_' . $width . ',h_' . $height . ',m_fast';
$data[$v] = $this->fileInfo->$key;
}
}
}
return $data;
}
}

View File

@@ -0,0 +1,448 @@
<?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\upload\storage;
use crmeb\basic\BaseUpload;
use crmeb\exceptions\UploadException;
use crmeb\services\DownloadImageService;
use think\exception\ValidateException;
use think\facade\Config;
use think\facade\Filesystem;
use think\File;
use think\Image;
/**
* 本地上传
* Class Local
* @package crmeb\services\upload\storage
*/
class Local extends BaseUpload
{
/**
* 默认存放路径
* @var string
*/
protected $defaultPath;
/**
* 缩略图、水印图存放位置
* @var string
*/
public $thumbWaterPath = 'thumb_water';
public function initialize(array $config)
{
parent::initialize($config);
$this->defaultPath = Config::get('filesystem.disks.' . Config::get('filesystem.default') . '.url');
$this->waterConfig['watermark_text_font'] = app()->getRootPath() . 'public' . '/statics/font/simsunb.ttf';
}
protected function app()
{
//
}
public function getTempKeys()
{
//
return $this->setError('请检查您的上传配置,视频默认oss上传');
}
/**
* 生成上传文件目录
* @param $path
* @param null $root
* @return string
*/
public function uploadDir($path, $root = null)
{
if ($root === null) $root = app()->getRootPath() . 'public/';
return str_replace('\\', '/', $root . 'uploads/' . $path);
}
/**
* 检查上传目录不存在则生成
* @param $dir
* @return bool
*/
protected function validDir($dir)
{
return is_dir($dir) == true || mkdir($dir, 0777, true) == true;
}
/**
* 检测filepath是否是远程地址
* @param string $filePath
* @return bool
*/
public function checkFilePathIsRemote(string $filePath)
{
return strpos($filePath, 'https:') !== false || strpos($filePath, 'http:') !== false || substr($filePath, 0, 2) === '//';
}
/**
* 生成与配置相关的文件名称以及路径
* @param string $filePath 原地址
* @param string $toPath 保存目录
* @param array $config 配置相关参数
* @param string $root
* @return string
*/
public function createSaveFilePath(string $filePath, string $toPath, array $config = [], $root = '/')
{
[$path, $ext] = $this->getFileName($filePath);
$fileName = md5(json_encode($config) . $filePath);
return $this->uploadDir($toPath, $root) . '/' . $fileName . '.' . $ext;
}
/**
* 文件上传
* @param string $file
* @return array|bool|mixed|\StdClass
*/
public function move(string $file = 'file', $realName = false)
{
$fileHandle = app()->request->file($file);
if (!$fileHandle) {
return $this->setError('Upload file does not exist');
}
if ($this->validate) {
try {
$error = [
$file . '.filesize' => 'Upload filesize error',
$file . '.fileExt' => 'Upload fileExt error',
$file . '.fileMime' => 'Upload fileMine error'
];
validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
} catch (ValidateException $e) {
return $this->setError($e->getMessage());
}
}
$stream = fopen($fileHandle->getPathname(), 'r');
$content = (fread($stream, filesize($fileHandle->getPathname())));
if (is_resource($stream)) {
fclose($stream);
}
// if (preg_match('/think|log|phar|Socket|Channel|Flysystem|Psr6Cache|Cached|Request|debug|Psr6Cachepool|eval/i', $content)) {
// return $this->setError('文件内容不合法');
// }
$fileType = pathinfo($fileHandle->getOriginalName(), PATHINFO_EXTENSION);
$fileName = Filesystem::putFileAs($this->path, $fileHandle, in_array($fileType, ['ico']) ? $fileHandle->getOriginalName() : strtolower($fileHandle->hashName()));
if (!$fileName)
return $this->setError('Upload failure');
$filePath = Filesystem::path($fileName);
$this->fileInfo->uploadInfo = new File($filePath);
$this->fileInfo->realName = $fileHandle->getOriginalName();
$this->fileInfo->fileName = $this->fileInfo->uploadInfo->getFilename();
$this->fileInfo->filePath = $this->defaultPath . '/' . str_replace('\\', '/', $fileName);
if ($this->checkImage(root_path() . 'public' . $this->fileInfo->filePath) && $this->authThumb && $fileType != 'ico') {
try {
$this->thumb($this->fileInfo->filePath);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
return $this->fileInfo;
}
/**
* 文件流上传
* @param string $fileContent
* @param string|null $key
* @return array|bool|mixed|\StdClass
*/
public function stream(string $fileContent, string $key = null)
{
if (!$key) {
$key = $this->saveFileName();
}
$this->verifyExtension($key);
$dir = $this->uploadDir($this->path);
if (!$this->validDir($dir)) {
return $this->setError('Failed to generate upload directory, please check the permission!');
}
$fileName = $dir . '/' . $key;
file_put_contents($fileName, $fileContent);
$this->fileInfo->uploadInfo = new File($fileName);
$this->fileInfo->realName = $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePath = $this->defaultPath . '/' . $this->path . '/' . $key;
if ($this->checkImage(root_path() . 'public' . $this->fileInfo->filePath) && $this->authThumb) {
try {
$this->thumb($this->fileInfo->filePath);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
return $this->fileInfo;
}
/**
* 文件流下载保存图片
* @param string $fileContent
* @param string|null $key
* @return array|bool|mixed|\StdClass
*/
public function down(string $fileContent, string $key = null)
{
if (!$key) {
$key = $this->saveFileName();
}
$dir = $this->uploadDir($this->path);
if (!$this->validDir($dir)) {
return $this->setError('Failed to generate upload directory, please check the permission!');
}
$fileName = $dir . '/' . $key;
file_put_contents($fileName, $fileContent);
$this->downFileInfo->downloadInfo = new File($fileName);
$this->downFileInfo->downloadRealName = $key;
$this->downFileInfo->downloadFileName = $key;
$this->downFileInfo->downloadFilePath = $this->defaultPath . '/' . $this->path . '/' . $key;
return $this->downFileInfo;
}
/**
* 生成缩略图
* @param string $filePath
* @param string $type
* @return array|mixed|string[]
*/
public function thumb(string $filePath = '', string $type = 'all')
{
$filePath = $this->getFilePath($filePath, true);
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
//地址存在且不是远程地址
if ($filePath && !$this->checkFilePathIsRemote($filePath) && strpos($filePath, 'uploads/' . $this->thumbWaterPath) === false) {
$dir = $this->uploadDir($this->thumbWaterPath);
if (!$this->validDir($dir)) {
throw new ValidateException('缩略图生成目录生成失败,目录:' . $dir);
}
$filePath = $this->water($filePath);
$this->fileInfo->filePathWater = $filePath;
$config = $this->thumbConfig;
try {
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
if (isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$savePath = $this->createSaveFilePath($filePath, $this->thumbWaterPath, [$height => $config[$height], $width => $config[$width]]);
//防止重复生成
if (!file_exists(root_path() . 'public' . $savePath)) {
if ($this->authThumb) {//生成
$Image = Image::open(app()->getRootPath() . 'public' . $filePath);
$Image->thumb($config[$width], $config[$height])->save(root_path() . 'public' . $savePath);
} else {
$savePath = $filePath;
}
}
$key = 'filePath' . ucfirst($v);
$data[$v] = $this->fileInfo->$key = $savePath;
}
}
}
} catch (\Throwable $e) {
throw new ValidateException($e->getMessage());
}
}
return $data;
}
/**
* 添加水印
* @param string $filePath
* @return mixed|string
*/
public function water(string $filePath = '')
{
$waterPath = $filePath = $this->getFilePath($filePath);
//地址存在且不是远程地址
if ($filePath && !$this->checkFilePathIsRemote($filePath)) {
$waterConfig = $this->waterConfig;
if ($waterConfig['image_watermark_status'] && $filePath) {
$waterPath = $this->createSaveFilePath($filePath, $this->thumbWaterPath, $waterConfig);
switch ($waterConfig['watermark_type']) {
case 1:
if ($waterConfig['watermark_image']) $waterPath = $this->image($filePath, $waterConfig, $waterPath);
break;
case 2:
$waterPath = $this->text($filePath, $waterConfig, $waterPath);
break;
}
}
}
return $waterPath;
}
/**
* 图片水印
* @param string $filePath
* @param array $waterConfig
* @param string $waterPath
* @return string
*/
public function image(string $filePath, array $waterConfig = [], string $waterPath = '')
{
if (!$waterConfig) {
$waterConfig = $this->waterConfig;
}
$watermark_image = $waterConfig['watermark_image'];
//远程图片
if ($watermark_image && $this->checkFilePathIsRemote($watermark_image)) {
//看是否在本地
if (!$this->isLocal($watermark_image)) {//不再本地 继续下载
[$p, $e] = $this->getFileName($watermark_image);
$name = 'water_image_' . md5($watermark_image) . '.' . $e;
$watermark_image = $this->defaultPath . '/' . $this->thumbWaterPath . '/' . $name;
if (!file_exists(root_path() . 'public' . $watermark_image)) {
try {
/** @var DownloadImageService $down */
$down = app()->make(DownloadImageService::class);
$data = $down->path($this->thumbWaterPath)->downloadImage($waterConfig['watermark_image'], $name);
$watermark_image = $data['path'] ?? '';
} catch (\Throwable $e) {
throw new ValidateException('远程水印图片下载失败,原因:' . $e->getMessage());
}
}
} else {
$watermark_image = $this->getFilePath($watermark_image, true);
}
}
if (!$watermark_image) {
throw new ValidateException('请先配置水印图片');
}
if (!$waterPath) {
[$path, $ext] = $this->getFileName($filePath);
$waterPath = $path . '_water_image.' . $ext;
}
try {
if (!file_exists(root_path() . 'public' . $waterPath)) {
$Image = Image::open(app()->getRootPath() . 'public' . $filePath);
$Image->water(app()->getRootPath() . 'public' . $watermark_image, $waterConfig['watermark_position'] ?: 1, $waterConfig['watermark_opacity'])->save(root_path() . 'public' . $waterPath);
}
} catch (\Throwable $e) {
throw new ValidateException($e->getMessage());
}
return $waterPath;
}
/**
* 文字水印
* @param string $filePath
* @param array $waterConfig
* @param string $waterPath
* @return string
*/
public function text(string $filePath, array $waterConfig = [], string $waterPath = '')
{
if (!$waterConfig) {
$waterConfig = $this->waterConfig;
}
if (!$waterConfig['watermark_text']) {
throw new ValidateException('请先配置水印文字');
}
if (!$waterPath) {
[$path, $ext] = $this->getFileName($filePath);
$waterPath = $path . '_water_text.' . $ext;
}
try {
if (!file_exists(root_path() . 'public' . $waterPath)) {
if ($this->authThumb) {//生成
$Image = Image::open(app()->getRootPath() . 'public' . $filePath);
if (strlen($waterConfig['watermark_text_color']) < 7) {
$waterConfig['watermark_text_color'] = substr($waterConfig['watermark_text_color'], 1);
$waterConfig['watermark_text_color'] = '#' . $waterConfig['watermark_text_color'] . $waterConfig['watermark_text_color'];
}
if (strlen($waterConfig['watermark_text_color']) > 7) {
$waterConfig['watermark_text_color'] = substr($waterConfig['watermark_text_color'], 0, 7);
}
$Image->text($waterConfig['watermark_text'], $waterConfig['watermark_text_font'], $waterConfig['watermark_text_size'], $waterConfig['watermark_text_color'], $waterConfig['watermark_position'], [$waterConfig['watermark_x'], $waterConfig['watermark_y'], $waterConfig['watermark_text_angle']])->save(root_path() . 'public' . $waterPath);
} else {
$waterPath = $filePath;
}
}
} catch (\Throwable $e) {
throw new ValidateException($e->getMessage() . $e->getLine());
}
return $waterPath;
}
/**
* 删除文件
* @param string $filePath
* @return bool|mixed
*/
public function delete(string $filePath)
{
if (file_exists($filePath)) {
try {
unlink($filePath);
$waterConfig = $this->waterConfig;
//水印图片
$waterPath = $this->createSaveFilePath($filePath, $this->thumbWaterPath, $waterConfig, null);
if (file_exists($waterPath)) unlink($waterPath);
$config = $this->thumbConfig;
//缩略图
foreach ($this->thumb as $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
if (isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$thumbPath = $this->createSaveFilePath($waterPath, $this->thumbWaterPath, [$height => $config[$height], $width => $config[$width]], null);
if (file_exists($thumbPath)) unlink($thumbPath);
}
}
return true;
} catch (UploadException $e) {
return $this->setError($e->getMessage());
}
}
return false;
}
public function listbuckets(string $region, bool $line = false, bool $shared = false)
{
return [];
}
public function createBucket(string $name, string $region)
{
return null;
}
public function deleteBucket(string $name)
{
return null;
}
public function setBucketCors(string $name, string $region)
{
return true;
}
public function getRegion()
{
return [];
}
public function bindDomian(string $name, string $domain, string $region = null)
{
return true;
}
public function getDomian(string $name, string $region = null)
{
return [];
}
}

View File

@@ -0,0 +1,385 @@
<?php
namespace crmeb\services\upload\storage;
use crmeb\exceptions\AdminException;
use crmeb\exceptions\UploadException;
use crmeb\services\upload\extend\obs\Client as TyClient;
use crmeb\basic\BaseUpload;
use DateTimeInterface;
use GuzzleHttp\Psr7\Utils;
use think\exception\ValidateException;
class Obs extends BaseUpload
{
/**
* accessKey
* @var mixed
*/
protected $accessKey;
/**
* secretKey
* @var mixed
*/
protected $secretKey;
/**
* 句柄
* @var TyClient
*/
protected $handle;
/**
* 空间域名 Domain
* @var mixed
*/
protected $uploadUrl;
/**
* 存储空间名称 公开空间
* @var mixed
*/
protected $storageName;
/**
* COS使用 所属地域
* @var mixed|null
*/
protected $storageRegion;
/**
* @var string
*/
protected $cdn;
/**
* @param string $file
* @param bool $isStream
* @param string|null $fileContent
* @return array|bool|mixed
*/
public function move(string $file = 'file', bool $isStream = false, string $fileContent = null)
{
if (!$isStream) {
$fileHandle = app()->request->file($file);
if (!$fileHandle) {
return $this->setError('上传的文件不存在');
}
if ($this->validate) {
try {
$error = [
$file . '.filesize' => 'Upload filesize error',
$file . '.fileExt' => 'Upload fileExt error',
$file . '.fileMime' => 'Upload fileMine error'
];
validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
} catch (ValidateException $e) {
return $this->setError($e->getMessage());
}
}
$key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
$body = fopen($fileHandle->getRealPath(), 'rb');
$body = (string)Utils::streamFor($body);
} else {
$key = $file;
$body = $fileContent;
}
$key = $this->getUploadPath($key);
try {
$uploadInfo = $this->app()->putObject($key, $body, 'application/octet-stream');
$this->fileInfo->uploadInfo = $uploadInfo;
$this->fileInfo->realName = $fileHandle->getOriginalName();
$this->fileInfo->filePath = ($this->cdn ?: $this->uploadUrl) . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 文件流上传
* @param string $fileContent
* @param string|null $key
* @return bool|mixed
*/
public function stream(string $fileContent, string $key = null)
{
if (!$key) {
$key = $this->saveFileName();
}
return $this->move($key, true, $fileContent);
}
public function delete(string $filePath)
{
try {
return $this->app()->deleteObject($filePath);
} catch (\Exception $e) {
return $this->setError($e->getMessage());
}
}
/**
* 初始化
* @param array $config
* @return mixed|void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->accessKey = $config['accessKey'] ?? null;
$this->secretKey = $config['secretKey'] ?? null;
$this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '');
$this->storageName = $config['storageName'] ?? null;
$this->storageRegion = $config['storageRegion'] ?? null;
$this->cdn = $config['cdn'] ?? null;
$this->waterConfig['watermark_text_font'] = 'simfang仿宋.ttf';
}
/**
* 实例化cos
* @return TyClient
*/
protected function app()
{
$this->handle = new TyClient([
'accessKey' => $this->accessKey,
'secretKey' => $this->secretKey,
'region' => $this->storageRegion ?: 'cn-north-1',
'bucket' => $this->storageName,
'uploadUrl' => $this->uploadUrl
]);
return $this->handle;
}
public function listbuckets(string $region = null, bool $line = false, bool $shared = false)
{
try {
$res = $this->app()->listBuckets();
return $res['Buckets']['Bucket'] ?? [];
} catch (\Throwable $e) {
return [];
}
}
public function createBucket(string $name, string $region, string $acl = TyClient::DEFAULT_OBS_ACL)
{
$regionData = $this->getRegion();
$regionData = array_column($regionData, 'value');
if (!in_array($region, $regionData)) {
return $this->setError('COS:无效的区域!');
}
$this->storageRegion = $region;
$app = $this->app();
//创建桶
try {
$app->createBucket($name, $region, $acl);
$data = [
'Statement' => [
'Sid' => '公共读' . $name,
'Effect' => 'Allow',
'Principal' => [
'ID' => ['*']
],
'Action' => ['HeadBucket', 'GetBucketLocation', 'ListBucketVersions', 'GetObject', 'RestoreObject', 'GetObjectVersion'],
'Resource' => [$name, $name . '/*']
]
];
$app->putPolicy($name, $region, $data);
} catch (\Throwable $e) {
return $this->setError('COS:' . $e->getMessage());
}
return true;
}
public function getRegion()
{
return $this->app()->getRegion();
}
public function deleteBucket(string $name, string $region = '')
{
try {
$this->app()->deleteBucket($name, $region);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
public function getDomian($name, $region)
{
try {
$res = $this->app()->getBucketDomain($name, $region);
if ($res) {
$domainRules = $res->toArray()['ListBucketCustomDomainsResult'];
return array_column($domainRules, 'DomainName');
} else {
return [];
}
} catch (\Throwable $e) {
}
return [];
}
public function bindDomian(string $name, string $domain, string $region = null)
{
$parseDomin = parse_url($domain);
try {
$this->app()->putBucketDomain($name, $region, [
'domainname' => $parseDomin['host'],
]);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
public function setBucketCors(string $name, string $region)
{
try {
$this->app()->putBucketCors($name, $region, [
'AllowedHeader' => ['*'],
'AllowedMethod' => ['PUT', 'GET', 'POST', 'DELETE', 'HEAD'],
'AllowedOrigin' => ['*'],
'ExposeHeader' => ['ETag'],
'MaxAgeSeconds' => 0
]);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* @return array
* @date 2023/6/13
*/
public function getTempKeys($callbackUrl = '', $dir = '')
{
// return [
// 'access_key' => $this->accessKey,
// 'secret_key' => $this->secretKey,
// 'type' => 'OBS'
// ];
// TODO: Implement getTempKeys() method.
$base64CallbackBody = base64_encode(json_encode([
'callbackUrl' => $callbackUrl,
'callbackBody' => 'filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}',
'callbackBodyType' => "application/x-www-form-urlencoded"
]));
$policy = json_encode([
'expiration' => $this->gmtIso8601(time() + 300),
'conditions' =>
[
[0 => 'content-length-range', 1 => 0, 2 => 1048576000],
['bucket' => $this->storageName],
[0 => 'starts-with', 1 => '$key', 2 => $dir],
]
]);
$base64Policy = base64_encode($policy);
$signature = base64_encode(hash_hmac('sha1', $base64Policy, $this->secretKey, true));
return [
'accessid' => $this->accessKey,
'host' => $this->uploadUrl,
'policy' => $base64Policy,
'signature' => $signature,
'expire' => time() + 30,
'callback' => $base64CallbackBody,
'cdn' => $this->cdn,
'type' => 'OBS'
];
}
/**
* 获取ISO时间格式
* @param $time
* @return string
* @throws \Exception
*/
protected function gmtIso8601($time): string
{
$dtStr = date("c", $time);
$myDateTime = new \DateTime($dtStr);
$expiration = $myDateTime->format(DateTimeInterface::ISO8601);
$pos = strpos($expiration, '+');
$expiration = substr($expiration, 0, $pos);
return $expiration . "Z";
}
/**
* 缩略图
* @param string $filePath
* @param string $fileName
* @param string $type
* @return array|mixed
*/
public function thumb(string $filePath = '', string $fileName = '', string $type = 'all')
{
$filePath = $this->getFilePath($filePath);
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
$config = $this->thumbConfig;
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
$key = 'filePath' . ucfirst($v);
if (sys_config('image_thumbnail_status', 1) && isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$this->fileInfo->$key = $filePath . '?x-oss-process=image/resize,h_' . $config[$height] . ',w_' . $config[$width];
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
} else {
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
}
}
}
}
return $data;
}
/**
* 水印
* @param string $filePath
* @return mixed|string
*/
public function water(string $filePath = '')
{
$filePath = $this->getFilePath($filePath);
$waterConfig = $this->waterConfig;
$waterPath = $filePath;
if ($waterConfig['image_watermark_status'] && $filePath) {
if (strpos($filePath, '?x-oss-process') === false) {
$filePath .= '?x-oss-process=image';
}
switch ($waterConfig['watermark_type']) {
case 1://图片
if (!$waterConfig['watermark_image']) {
throw new AdminException('请先配置水印图片');
}
$waterPath = $filePath .= '/watermark,image_' . base64_encode($waterConfig['watermark_image']) . ',t_' . $waterConfig['watermark_opacity'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
break;
case 2://文字
if (!$waterConfig['watermark_text']) {
throw new AdminException('请先配置水印文字');
}
$waterConfig['watermark_text_color'] = str_replace('#', '', $waterConfig['watermark_text_color']);
$waterPath = $filePath .= '/watermark,text_' . base64_encode($waterConfig['watermark_text']) . ',color_' . $waterConfig['watermark_text_color'] . ',size_' . $waterConfig['watermark_text_size'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
break;
}
}
return $waterPath;
}
}

View File

@@ -0,0 +1,586 @@
<?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\upload\storage;
use crmeb\basic\BaseUpload;
use crmeb\exceptions\UploadException;
use GuzzleHttp\Psr7\Utils;
use OSS\Core\OssException;
use OSS\Model\CorsConfig;
use OSS\Model\CorsRule;
use OSS\OssClient;
use think\exception\ValidateException;
/**
* 阿里云OSS上传
* Class OSS
*/
class Oss extends BaseUpload
{
/**
* accessKey
* @var mixed
*/
protected $accessKey;
/**
* secretKey
* @var mixed
*/
protected $secretKey;
/**
* 句柄
* @var \OSS\OssClient
*/
protected $handle;
/**
* 空间域名 Domain
* @var mixed
*/
protected $uploadUrl;
/**
* 存储空间名称 公开空间
* @var mixed
*/
protected $storageName;
/**
* COS使用 所属地域
* @var mixed|null
*/
protected $storageRegion;
/**
* 水印位置
* @var string[]
*/
protected $position = [
'1' => 'nw',//:左上
'2' => 'north',//:中上
'3' => 'ne',//:右上
'4' => 'west',//:左中
'5' => 'center',//:中部
'6' => 'east',//:右中
'7' => 'sw',//:左下
'8' => 'south',//:中下
'9' => 'se',//:右下
];
/**
* 初始化
* @param array $config
* @return mixed|void
*/
protected function initialize(array $config)
{
parent::initialize($config);
$this->accessKey = $config['accessKey'] ?? null;
$this->secretKey = $config['secretKey'] ?? null;
$this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '');
$this->storageName = $config['storageName'] ?? null;
$this->storageRegion = $config['storageRegion'] ?? null;
}
/**
* 初始化oss
* @return OssClient
* @throws OssException
*/
protected function app()
{
if (!$this->accessKey || !$this->secretKey) {
throw new UploadException('Please configure accessKey and secretKey');
}
$this->handle = new OssClient($this->accessKey, $this->secretKey, $this->storageRegion);
//不再自动创建
// if (!$this->handle->doesBucketExist($this->storageName)) {
// $this->handle->createBucket($this->storageName, OssClient::OSS_ACL_TYPE_PUBLIC_READ_WRITE);
// }
return $this->handle;
}
/**
* 上传文件
* @param string $file
* @return array|bool|mixed|\StdClass
*/
public function move(string $file = 'file')
{
$fileHandle = app()->request->file($file);
if (!$fileHandle) {
return $this->setError('Upload file does not exist');
}
if ($this->validate) {
try {
$error = [
$file . '.filesize' => 'Upload filesize error',
$file . '.fileExt' => 'Upload fileExt error',
$file . '.fileMime' => 'Upload fileMine error'
];
validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
} catch (ValidateException $e) {
return $this->setError($e->getMessage());
}
}
$key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
$key = $this->getUploadPath($key);
try {
$uploadInfo = $this->app()->putObject($this->storageName, $key, file_get_contents($fileHandle->getRealPath()));
if (!isset($uploadInfo['info']['url'])) {
return $this->setError('Upload failure');
}
$this->fileInfo->uploadInfo = $uploadInfo;
$this->fileInfo->realName = $fileHandle->getOriginalName();
$this->fileInfo->filePath = $this->uploadUrl . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (UploadException $e) {
return $this->setError($e->getMessage());
}
}
/**
* 文件流上传
* @param string $fileContent
* @param string|null $key
* @return bool|mixed
*/
public function stream(string $fileContent, string $key = null)
{
try {
if (!$key) {
$key = $this->saveFileName();
}
$key = $this->getUploadPath($key);
$this->verifyExtension($key);
$fileContent = (string)Utils::streamFor($fileContent);
$uploadInfo = $this->app()->putObject($this->storageName, $key, $fileContent);
if (!isset($uploadInfo['info']['url'])) {
return $this->setError('Upload failure');
}
$this->fileInfo->uploadInfo = $uploadInfo;
$this->fileInfo->realName = $key;
$this->fileInfo->filePath = $this->uploadUrl . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (UploadException $e) {
return $this->setError($e->getMessage());
}
}
/**
* 缩略图
* @param string $filePath
* @param string $type
* @return mixed|string[]
*/
public function thumb(string $filePath = '', string $type = 'all')
{
$filePath = $this->getFilePath($filePath);
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
$config = $this->thumbConfig;
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
if (isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$key = 'filePath' . ucfirst($v);
$this->fileInfo->$key = $filePath . '?x-oss-process=image/resize,h_' . $config[$height] . ',w_' . $config[$width];
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
}
}
}
}
return $data;
}
/**
* 水印
* @param string $filePath
* @return mixed|string
*/
public function water(string $filePath = '')
{
$filePath = $this->getFilePath($filePath);
$waterConfig = $this->waterConfig;
$waterPath = $filePath;
if ($waterConfig['image_watermark_status'] && $filePath) {
if (strpos($filePath, '?x-oss-process') === false) {
$filePath .= '?x-oss-process=image';
}
switch ($waterConfig['watermark_type']) {
case 1://图片
if (!$waterConfig['watermark_image']) {
throw new ValidateException('请先配置水印图片');
}
$waterPath = $filePath .= '/watermark,image_' . base64_encode($waterConfig['watermark_image']) . ',t_' . $waterConfig['watermark_opacity'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
break;
case 2://文字
if (!$waterConfig['watermark_text']) {
throw new ValidateException('请先配置水印文字');
}
$waterConfig['watermark_text_color'] = str_replace('#', '', $waterConfig['watermark_text_color']);
$waterPath = $filePath .= '/watermark,text_' . base64_encode($waterConfig['watermark_text']) . ',color_' . $waterConfig['watermark_text_color'] . ',size_' . $waterConfig['watermark_text_size'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
break;
}
}
return $waterPath;
}
/**
* 获取视频封面图
* @param string $filePath
* @param string $type
* @param int $time
* @return array
*/
public function videoCoverImage(string $filePath = '', string $type = 'all', int $time = 1)
{
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
//?x-oss-process=video/snapshot,t_7000,f_jpg,w_800,h_600,m_fast
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 600;
$width = 400;
$key = 'filePath' . ucfirst($v);
$this->fileInfo->$key = $filePath . '?x-oss-process=video/snapshot,t_' . ($time * 1000) . ',f_jpg,w_' . $width . ',h_' . $height . ',m_fast';
$data[$v] = $this->fileInfo->$key;
}
}
}
return $data;
}
/**
* 删除资源
* @param $key
* @return mixed
*/
public function delete(string $key)
{
try {
return $this->app()->deleteObject($this->storageName, $key);
} catch (OssException $e) {
return $this->setError($e->getMessage());
}
}
/**
* 获取OSS上传密钥
* @return mixed|void
*/
public function getTempKeys($callbackUrl = '', $dir = '')
{
//
$base64CallbackBody = base64_encode(json_encode([
'callbackUrl' => $callbackUrl,
'callbackBody' => 'filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}',
'callbackBodyType' => "application/x-www-form-urlencoded"
]));
$policy = json_encode([
'expiration' => $this->gmtIso8601(time() + 30),
'conditions' =>
[
[0 => 'content-length-range', 1 => 0, 2 => 1048576000],
[0 => 'starts-with', 1 => '$key', 2 => $dir]
]
]);
$base64Policy = base64_encode($policy);
$signature = base64_encode(hash_hmac('sha1', $base64Policy, $this->secretKey, true));
return [
'accessid' => $this->accessKey,
'host' => $this->uploadUrl,
'policy' => $base64Policy,
'signature' => $signature,
'expire' => time() + 30,
'callback' => $base64CallbackBody,
'type' => 'OSS'
];
}
/**
* 获取ISO时间格式
* @param $time
* @return string
*/
protected function gmtIso8601($time)
{
$dtStr = date("c", $time);
$mydatetime = new \DateTime($dtStr);
$expiration = $mydatetime->format(\DateTime::ISO8601);
$pos = strpos($expiration, '+');
$expiration = substr($expiration, 0, $pos);
return $expiration . "Z";
}
/**
* 获取当前管理下的所有桶
* @param string|null $region
* @param bool $line
* @param bool $shared
* @return mixed|\OSS\Model\BucketListInfo
* @throws OssException
*/
public function listbuckets(string $region = 'oss-cn-hangzhou.aliyuncs.com', bool $line = false, bool $shared = false)
{
$handle = new OssClient($this->accessKey, $this->secretKey, $region);
$response = $handle->listBuckets();
$data = $response->getBucketList();
$list = [];
foreach ($data as $item) {
$list[] = [
'location' => $item->getLocation(),
'name' => $item->getName(),
'createTime' => $item->getCreateDate()
];
}
return $list;
}
/**
* @param string $name
* @param string $region
* @param string $acl
* @return mixed|void
*/
public function createBucket(string $name, string $region = '', string $acl = OssClient::OSS_ACL_TYPE_PUBLIC_READ)
{
$regionData = $this->getRegion();
if (!in_array($region, array_column($regionData, 'value'))) {
return $this->setError('OSS:无效的区域');
}
try {
$handle = new OssClient($this->accessKey, $this->secretKey, $region);
if ($handle->doesBucketExist($name)) {
return $this->setError('OSS:空间已经存在,请更换空间名');
}
return $handle->createBucket($name, $acl);
} catch (\Throwable $e) {
if (strstr('The bucket you access does not belong to you', $e->getMessage())) {
return $this->setError('OSS:空间已被使用,请更换空间名');
}
return $this->setError('OSS:' . $e->getMessage());
}
}
/**
* @param string $name
* @param string $region
* @return bool|mixed|null
*/
public function deleteBucket(string $name, string $region = '')
{
try {
$handle = new OssClient($this->accessKey, $this->secretKey, $region);
return $handle->deleteBucket($name);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* @param string $name
* @param string|null $region
* @return array|bool
*/
public function getDomian(string $name, string $region = null)
{
try {
$handle = new OssClient($this->accessKey, $this->secretKey, $region);
$res = $handle->getBucketCname($name);
$data = $res->getCnames();
return array_column($data, 'Domain');
} catch (\Throwable $e) {
}
return [];
}
/**
* 绑定域名
* @param string $name
* @param string $domain
* @param string|null $region
* @return bool|mixed
*/
public function bindDomian(string $name, string $domain, string $region = null)
{
$parseDomin = parse_url($domain);
try {
$handle = new OssClient($this->accessKey, $this->secretKey, $region);
$res = $handle->getBucketCname($name);
$data = $res->getCnames();
if (in_array($parseDomin['host'], array_column($data, 'Domain'))) {
return true;
}
return $handle->addBucketCname($name, $parseDomin['host']);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 设置跨域
* @param string $name
* @param string $region
* @return bool|null
*/
public function setBucketCors(string $name, string $region)
{
try {
$handle = new OssClient($this->accessKey, $this->secretKey, $region);
$corsConfig = new CorsConfig();
$rule = new CorsRule();
// 设置允许跨域请求的响应头。AllowedHeader可以设置多个,每个AllowedHeader中最多只能使用一个通配符星号(*)。
// 建议无特殊需求时设置AllowedHeader为星号(*)。
$rule->addAllowedHeader("*");
// 设置允许用户从应用程序中访问的响应头。ExposeHeader可以设置多个,ExposeHeader中不支持使用通配符星号(*)。
$rule->addExposeHeader("ETag");
// 设置允许的跨域请求的来源。AllowedOrigin可以设置多个,每个AllowedOrigin中最多只能使用一个通配符星号(*)。
// 设置AllowedOrigin为星号(*)时,表示允许所有域的来源。
$rule->addAllowedOrigin("*");
// 设置允许的跨域请求方法。
$rule->addAllowedMethod("POST");
$rule->addAllowedMethod("GET");
$rule->addAllowedMethod("DELETE");
$rule->addAllowedMethod("PUT");
$rule->addAllowedMethod("HEAD");
// 设置浏览器对特定资源的预取(OPTIONS)请求返回结果的缓存时间,单位为秒。
$rule->setMaxAgeSeconds(600);
// 每个Bucket最多支持添加10条规则。
$corsConfig->addRule($rule);
return $handle->putBucketCors($name, $corsConfig);
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 数据中心
* @return mixed|\string[][]
*/
public function getRegion()
{
return [
[
'value' => 'oss-cn-hangzhou.aliyuncs.com',
'label' => '华东1(杭州)'
],
[
'value' => 'oss-cn-shanghai.aliyuncs.com',
'label' => '华东2(上海)'
],
[
'value' => 'oss-cn-qingdao.aliyuncs.com',
'label' => '华北1(青岛)'
],
[
'value' => 'oss-cn-beijing.aliyuncs.com',
'label' => '华北2(北京)'
],
[
'value' => 'oss-cn-zhangjiakou.aliyuncs.com',
'label' => '华北 3(张家口)'
],
[
'value' => 'oss-cn-huhehaote.aliyuncs.com',
'label' => '华北5(呼和浩特)'
],
[
'value' => 'oss-cn-wulanchabu.aliyuncs.com',
'label' => '华北6(乌兰察布)'
],
[
'value' => 'oss-cn-shenzhen.aliyuncs.com',
'label' => '华南1(深圳)'
],
[
'value' => 'oss-cn-heyuan.aliyuncs.com',
'label' => '华南2(河源)'
],
[
'value' => 'oss-cn-guangzhou.aliyuncs.com',
'label' => '华南3(广州)'
],
[
'value' => 'oss-cn-chengdu.aliyuncs.com',
'label' => '西南1(成都)'
],
[
'value' => 'oss-cn-hongkong.aliyuncs.com',
'label' => '中国(香港)'
],
[
'value' => 'oss-us-west-1.aliyuncs.com',
'label' => '美国(硅谷)*'
],
[
'value' => 'oss-us-east-1.aliyuncs.com',
'label' => '美国(弗吉尼亚)*'
],
[
'value' => 'oss-ap-southeast-1.aliyuncs.com',
'label' => '新加坡*'
],
[
'value' => 'oss-ap-southeast-2.aliyuncs.com',
'label' => '澳大利亚(悉尼)*'
],
[
'value' => 'oss-ap-southeast-3.aliyuncs.com',
'label' => '马来西亚(吉隆坡)*'
],
[
'value' => 'oss-ap-southeast-5.aliyuncs.com',
'label' => '印度尼西亚(雅加达)*'
],
[
'value' => 'oss-ap-northeast-1.aliyuncs.com',
'label' => '日本(东京)*'
],
[
'value' => 'oss-ap-south-1.aliyuncs.com',
'label' => '印度(孟买)*'
],
[
'value' => 'oss-eu-central-1.aliyuncs.com',
'label' => '德国(法兰克福)*'
],
[
'value' => 'oss-eu-west-1.aliyuncs.com',
'label' => '英国(伦敦)'
],
[
'value' => 'oss-me-east-1.aliyuncs.com',
'label' => '阿联酋(迪拜)*'
],
[
'value' => 'oss-ap-southeast-6.aliyuncs.com',
'label' => '菲律宾(马尼拉)'
]
];
}
}

View File

@@ -0,0 +1,493 @@
<?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\upload\storage;
use crmeb\basic\BaseUpload;
use crmeb\exceptions\UploadException;
use crmeb\services\HttpService;
use Qiniu\Auth;
use Qiniu\Http\Client;
use Qiniu\Http\Error;
use Qiniu\Storage\BucketManager;
use Qiniu\Storage\UploadManager;
use Qiniu\Config;
use think\exception\ValidateException;
/**
* 七牛云上传
* Class Qiniu
*/
class Qiniu extends BaseUpload
{
/**
* accessKey
* @var mixed
*/
protected $accessKey;
/**
* secretKey
* @var mixed
*/
protected $secretKey;
/**
* 句柄
* @var object
*/
protected $handle;
/**
* 空间域名 Domain
* @var mixed
*/
protected $uploadUrl;
/**
* 存储空间名称 公开空间
* @var mixed
*/
protected $storageName;
/**
* COS使用 所属地域
* @var mixed|null
*/
protected $storageRegion;
/**
* 水印位置
* @var string[]
*/
protected $position = [
'1' => 'NorthWest',//:左上
'2' => 'North',//:中上
'3' => 'NorthEast',//:右上
'4' => 'West',//:左中
'5' => 'Center',//:中部
'6' => 'East',//:右中
'7' => 'SouthWest',//:左下
'8' => 'South',//:中下
'9' => 'SouthEast',//:右下
];
/**
* 初始化
* @param array $config
* @return mixed|void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->accessKey = $config['accessKey'] ?? null;
$this->secretKey = $config['secretKey'] ?? null;
$this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '');
$this->storageName = $config['storageName'] ?? null;
$this->storageRegion = $config['storageRegion'] ?? null;
}
/**
* 实例化七牛云
* @return object|Auth
*/
protected function app()
{
if (!$this->accessKey || !$this->secretKey) {
throw new UploadException('Please configure accessKey and secretKey');
}
$this->handle = new Auth($this->accessKey, $this->secretKey);
return $this->handle;
}
/**
* 上传文件
* @param string $file
* @return array|bool|mixed|\StdClass|string
*/
public function move(string $file = 'file')
{
$fileHandle = app()->request->file($file);
if (!$fileHandle) {
return $this->setError('Upload file does not exist');
}
if ($this->validate) {
try {
$error = [
$file . '.filesize' => 'Upload filesize error',
$file . '.fileExt' => 'Upload fileExt error',
$file . '.fileMime' => 'Upload fileMine error'
];
validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
} catch (ValidateException $e) {
return $this->setError($e->getMessage());
}
}
$key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
$key = $this->getUploadPath($key);
$token = $this->app()->uploadToken($this->storageName);
try {
$uploadMgr = new UploadManager();
[$result, $error] = $uploadMgr->putFile($token, $key, $fileHandle->getRealPath());
if ($error !== null) {
return $this->setError($error->message());
}
$this->fileInfo->uploadInfo = $result;
$this->fileInfo->realName = $fileHandle->getOriginalName();
$this->fileInfo->filePath = $this->uploadUrl . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (UploadException $e) {
return $this->setError($e->getMessage());
}
}
/**
* 文件流上传
* @param string $fileContent
* @param string|null $key
* @return array|bool|mixed|\StdClass
*/
public function stream(string $fileContent, string $key = null)
{
if (!$key) {
$key = $this->saveFileName();
}
$key = $this->getUploadPath($key);
$this->verifyExtension($key);
$token = $this->app()->uploadToken($this->storageName, $key);
try {
$uploadMgr = new UploadManager();
[$result, $error] = $uploadMgr->put($token, $key, $fileContent);
if ($error !== null) {
return $this->setError($error->message());
}
$this->fileInfo->uploadInfo = $result;
$this->fileInfo->realName = $key;
$this->fileInfo->filePath = $this->uploadUrl . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (UploadException $e) {
return $this->setError($e->getMessage());
}
}
/**
* 缩略图
* @param string $filePath
* @param string $type
* @return mixed|string[]
*/
public function thumb(string $filePath = '', string $type = 'all')
{
$filePath = $this->getFilePath($filePath);
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
$config = $this->thumbConfig;
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
if (isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$key = 'filePath' . ucfirst($v);
$this->fileInfo->$key = $filePath . '?imageView2/2/w/' . $config[$width] . '/h/' . $config[$height];
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
}
}
}
}
return $data;
}
/**
* 水印
* @param string $filePath
* @return mixed|string
*/
public function water(string $filePath = '')
{
$filePath = $this->getFilePath($filePath);
$waterConfig = $this->waterConfig;
$waterPath = $filePath;
if ($waterConfig['image_watermark_status'] && $filePath) {
if (strpos($filePath, '?') === false) {
$filePath .= '?watermark';
} else {
$filePath .= '&watermark';
}
switch ($waterConfig['watermark_type']) {
case 1://图片
if (!$waterConfig['watermark_image']) {
throw new ValidateException('请先配置水印图片');
}
$waterPath = $filePath .= '/1/image/' . base64_encode($waterConfig['watermark_image']) . '/gravity/' . ($this->position[$waterConfig['watermark_position']] ?? 'SouthEest') . '/dissolve/' . $waterConfig['watermark_opacity'] . '/dx/' . $waterConfig['watermark_x'] . '/dy/' . $waterConfig['watermark_y'];
break;
case 2://文字
if (!$waterConfig['watermark_text']) {
throw new ValidateException('请先配置水印文字');
}
$waterPath = $filePath .= '/2/text/' . base64_encode($waterConfig['watermark_text']) . '/fill/' . base64_encode($waterConfig['watermark_text_color']) . '/fontsize/' . $waterConfig['watermark_text_size'] . '/gravity/' . ($this->position[$waterConfig['watermark_position']] ?? 'SouthEest') . '/dx/' . $waterConfig['watermark_x'] . '/dy/' . $waterConfig['watermark_y'];
break;
}
}
return $waterPath;
}
/**
* 获取视频封面图
* @param string $filePath
* @param string $type
* @param int $time
* @return array
*/
public function videoCoverImage(string $filePath = '', string $type = 'all', int $time = 1)
{
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
//?fops=vframe/jpg/offset/7/w/480/h/360
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 600;
$width = 400;
$key = 'filePath' . ucfirst($v);
$this->fileInfo->$key = $filePath . '?fops=vframe/jpg/offset/' . $time . '/w/' . $width . '/h/' . $height;
$data[$v] = $this->fileInfo->$key;
}
}
}
return $data;
}
/**
* 获取上传配置信息
* @return array
*/
public function getSystem()
{
$token = $this->app()->uploadToken($this->storageName);
$domain = $this->uploadUrl;
$key = $this->saveFileName();
return compact('token', 'domain', 'key');
}
/**
* 删除资源
* @param $key
* @param $bucket
* @return mixed
*/
public function delete(string $key)
{
$bucketManager = new BucketManager($this->app(), new Config());
return $bucketManager->delete($this->storageName, $key);
}
/**
* 获取七牛云上传密钥
* @return mixed|string
*/
public function getTempKeys()
{
$token = $this->app()->uploadToken($this->storageName);
$domain = $this->uploadUrl;
$key = $this->saveFileName(NULL, 'mp4');
$type = 'QINIU';
return compact('token', 'domain', 'key', 'type');
}
/**
* 获取当前所有桶列表
* @param string|null $region
* @param bool $line
* @param bool $shared
* @return bool|mixed
*/
public function listbuckets(string $region = null, bool $line = false, bool $shared = false)
{
$bucket = new BucketManager($this->app());
[$response, $error] = $bucket->listbuckets($region, $line ? 'true' : 'false', $shared ? 'true' : 'false');
if ($error !== null) {
return $this->setError($error->message());
}
return $response;
}
/**
* @param string $name
* @param string $region
* @return bool|mixed
*/
public function createBucket(string $name, string $region = 'z0')
{
$regionData = $this->getRegion();
if (!in_array($region, array_column($regionData, 'value'))) {
return $this->setError('七牛云:无效的区域');
}
$url = 'https://' . Config::UC_HOST . '/mkbucketv3/' . $name . '/region/' . $region;
$body = null;
$headers = $this->app()->authorizationV2($url, 'POST', $body, 'application/json');
$headers["Content-Type"] = 'application/json';
$ret = Client::post($url, $body, $headers);
if (!$ret->ok()) {
$error = new Error($url, $ret);
if ('bucket exists' === $error->message()) {
return $this->setError('七牛云:云空间已存在');
}
return $this->setError('七牛云:' . $error->message());
}
return ($ret->body === null) ? array() : $ret->json();
}
/**
* 获取区域
* @return mixed|\string[][]
*/
public function getRegion()
{
return [
[
'value' => 'z0',
'label' => '华东'
],
[
'value' => 'z1',
'label' => '华北'
],
[
'value' => 'z2',
'label' => '华南'
],
[
'value' => 'na0',
'label' => '北美'
],
[
'value' => 'as0',
'label' => '东南亚'
],
[
'value' => 'cn-east-2',
'label' => '华东-浙江2'
],
];
}
/**
* 删除空间
* @param string $name
* @return bool|mixed
*/
public function deleteBucket(string $name)
{
$bucket = new BucketManager($this->app());
[$response, $error] = $bucket->deleteBucket($name);
if ($error !== null) {
return $this->setError($error->message());
}
return $response;
}
/**
* 获取七牛域名
* @param string $name
* @return array|bool|mixed|null
*/
public function getDomian(string $name)
{
$url = 'https://' . Config::UC_HOST . '/v2/domains?tbl=' . $name;
$body = null;
$headers = $this->app()->authorizationV2($url, 'POST', $body, 'application/x-www-form-urlencoded');
$headers["Content-Type"] = 'application/x-www-form-urlencoded';
$ret = Client::post($url, $body, $headers);
if (!$ret->ok()) {
$error = new Error($url, $ret);
return $this->setError('七牛云:' . $error->message());
}
return ($ret->body === null) ? array() : $ret->json();
}
public function getDomianInfo(string $host)
{
$url = 'https://' . Config::API_HOST . '/domain/' . $host;
$headers = $this->app()->authorization($url, null, 'application/x-www-form-urlencoded');
$headers["Content-Type"] = 'application/x-www-form-urlencoded';
$ret = Client::get($url, $headers);
if (!$ret->ok()) {
$error = new Error($url, $ret);
return $this->setError('七牛云:' . $error->message());
}
return ($ret->body === null) ? array() : $ret->json();
}
/**
*
* @param string $name
* @param string $domain
* @param string|null $region
* @return array|bool|mixed|null
*/
public function bindDomian(string $name, string $domain, string $region = null)
{
$parseDomin = parse_url($domain);
$url = 'https://' . Config::API_HOST . '/domain/' . $parseDomin['host'];
$body = [
'type' => 'normal',
'platform' => 'web',
'geocover' => 'china',
'source' => [
'sourceType' => 'qiniuBucket',
'sourceQiniuBucket' => $name,
'TestURLPath' => 'qiniu_do_not_delete.gif',
],
'protocol' => $parseDomin['scheme'],
'cache' => [
'cacheControls' => [
[
'time' => 1,
'timeunit' => 4,
'type' => 'all',
'rule' => '*'
]
],
'ignoreParam' => false
]
];
$bodyJson = json_encode($body);
$headers = $this->app()->authorization($url, $bodyJson, 'application/json');
$headers["Content-Type"] = 'application/json';
$ret = Client::post($url, $bodyJson, $headers);
if (!$ret->ok()) {
$error = new Error($url, $ret);
return $this->setError('七牛云:' . $error->message());
}
return ($ret->body === null) ? array() : $ret->json();
}
/**
* 跨域
* @param string $name
* @param string $region
* @return bool
*/
public function setBucketCors(string $name, string $region)
{
return true;
}
}

View File

@@ -0,0 +1,513 @@
<?php
namespace crmeb\services\upload\storage;
use crmeb\exceptions\AdminException;
use crmeb\services\upload\extend\obs\Client as TyClient;
use crmeb\basic\BaseUpload;
use DateTimeInterface;
use GuzzleHttp\Psr7\Utils;
use think\exception\ValidateException;
class Tyoss extends BaseUpload
{
/**
* accessKey
* @var mixed
*/
protected $accessKey;
/**
* secretKey
* @var mixed
*/
protected $secretKey;
/**
* 句柄
* @var TyClient
*/
protected $handle;
/**
* 空间域名 Domain
* @var mixed
*/
protected $uploadUrl;
/**
* 存储空间名称 公开空间
* @var mixed
*/
protected $storageName;
/**
* COS使用 所属地域
* @var mixed|null
*/
protected $storageRegion;
/**
* @var string
*/
protected $cdn;
/**
* @param string $file
* @param bool $isStream
* @param string|null $fileContent
* @return array|bool|mixed
*/
public function move(string $file = 'file', bool $isStream = false, string $fileContent = null)
{
if (!$isStream) {
$fileHandle = app()->request->file($file);
if (!$fileHandle) {
return $this->setError('上传的文件不存在');
}
if ($this->validate) {
try {
$error = [
$file . '.filesize' => 'Upload filesize error',
$file . '.fileExt' => 'Upload fileExt error',
$file . '.fileMime' => 'Upload fileMine error'
];
validate([$file => $this->validate], $error)->check([$file => $fileHandle]);
} catch (ValidateException $e) {
return $this->setError($e->getMessage());
}
}
$key = $this->saveFileName($fileHandle->getRealPath(), $fileHandle->getOriginalExtension());
$body = fopen($fileHandle->getRealPath(), 'rb');
$body = (string)Utils::streamFor($body);
} else {
$key = $file;
$body = $fileContent;
}
$key = $this->getUploadPath($key);
try {
$uploadInfo = $this->app()->putObject($key, $body, 'application/octet-stream');
$this->fileInfo->uploadInfo = $uploadInfo;
$this->fileInfo->realName = $fileHandle->getOriginalName();
$this->fileInfo->filePath = ($this->cdn ?: $this->uploadUrl) . '/' . $key;
$this->fileInfo->fileName = $key;
$this->fileInfo->filePathWater = $this->water($this->fileInfo->filePath);
$this->authThumb && $this->thumb($this->fileInfo->filePath);
return $this->fileInfo;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* 文件流上传
* @param string $fileContent
* @param string|null $key
* @return bool|mixed
*/
public function stream(string $fileContent, string $key = null)
{
if (!$key) {
$key = $this->saveFileName();
}
return $this->move($key, true, $fileContent);
}
public function delete(string $filePath)
{
try {
return $this->app()->deleteObject($filePath);
} catch (\Exception $e) {
return $this->setError($e->getMessage());
}
}
/**
* 初始化
* @param array $config
* @return mixed|void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->accessKey = $config['accessKey'] ?? null;
$this->secretKey = $config['secretKey'] ?? null;
$this->uploadUrl = $this->checkUploadUrl($config['uploadUrl'] ?? '');
$this->storageName = $config['storageName'] ?? null;
$this->storageRegion = $config['storageRegion'] ?? null;
$this->cdn = $config['cdn'] ?? null;
$this->waterConfig['watermark_text_font'] = 'simfang仿宋.ttf';
}
/**
* 实例化cos
* @return TyClient
*/
protected function app()
{
$this->handle = new TyClient([
'accessKey' => $this->accessKey,
'secretKey' => $this->secretKey,
'region' => $this->storageRegion ?: 'cn-qhxn1',
'bucket' => $this->storageName,
'uploadUrl' => $this->uploadUrl,
'type' => 'ty'
]);
return $this->handle;
}
public function listbuckets(string $region = null, bool $line = false, bool $shared = false)
{
try {
$res = $this->app()->listBuckets();
return $res['Buckets']['Bucket'] ?? [];
} catch (\Throwable $e) {
return [];
}
}
public function createBucket(string $name, string $region, string $acl = TyClient::DEFAULT_OBS_ACL)
{
$regionData = $this->getRegion();
$regionData = array_column($regionData, 'value');
if (!in_array($region, $regionData)) {
return $this->setError('COS:无效的区域!');
}
$this->storageRegion = $region;
$app = $this->app();
//创建桶
try {
$app->createBucket($name, $region, $acl);
$data = [
'Statement' => [
'Sid' => '公共读' . $name,
'Effect' => 'Allow',
'Principal' => [
'ID' => ['*']
],
'Action' => ['HeadBucket', 'GetBucketLocation', 'ListBucketVersions', 'GetObject', 'RestoreObject', 'GetObjectVersion'],
'Resource' => [$name, $name . '/*']
]
];
$app->putPolicy($name, $region, $data);
} catch (\Throwable $e) {
return $this->setError('COS:' . $e->getMessage());
}
return true;
}
public function getRegion()
{
return [
[
'value' => 'cn-gz1',
'label' => '贵州'
],
[
'value' => 'cn-fz1',
'label' => '福州'
],
[
'value' => 'cn-hz1',
'label' => '杭州'
],
[
'value' => 'cn-sz1',
'label' => '深圳'
],
[
'value' => 'cn-gdgz1',
'label' => '广州'
],
[
'value' => 'cn-jssz1',
'label' => '苏州'
],
[
'value' => 'cn-sh1',
'label' => '上海'
],
[
'value' => 'cn-ahwh1',
'label' => '芜湖'
],
[
'value' => 'cn-bj1',
'label' => '北京'
],
[
'value' => 'cn-sccd1',
'label' => '成都'
],
[
'value' => 'cn-hazz1',
'label' => '郑州'
],
[
'value' => 'cn-hncs1',
'label' => '长沙'
],
[
'value' => 'cn-gxnn1',
'label' => '南宁'
],
[
'value' => 'cn-jxnc1',
'label' => '南昌'
],
[
'value' => 'cn-sdqd1',
'label' => '青岛'
],
[
'value' => 'cn-snxy1',
'label' => '咸阳'
],
[
'value' => 'cn-xjcj1',
'label' => '新疆'
],
[
'value' => 'cn-ynkm1',
'label' => '昆明'
],
[
'value' => 'cn-hihk1',
'label' => '海口'
],
[
'value' => 'cn-hbwh1',
'label' => '武汉'
],
[
'value' => 'cn-cq1',
'label' => '重庆'
],
[
'value' => 'cn-qhxn1',
'label' => '西宁'
],
[
'value' => 'cn-gslz1',
'label' => '兰州'
],
[
'value' => 'cn-nxyc1',
'label' => '银川'
],
[
'value' => 'cn-sxty1',
'label' => '太原'
],
[
'value' => 'cn-hesjz1',
'label' => '石家庄'
],
[
'value' => 'cn-tj1',
'label' => '天津'
],
[
'value' => 'cn-jlcc1',
'label' => '长春'
],
[
'value' => 'cn-hlhrb1',
'label' => '哈尔滨'
],
[
'value' => 'cn-nmhh1',
'label' => '内蒙古'
],
[
'value' => 'cn-lnsy1',
'label' => '沈阳'
],
[
'value' => 'cn-north1',
'label' => '华北'
]
];
}
public function deleteBucket(string $name, string $region = '')
{
try {
$this->app()->deleteBucket($name, $region);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
public function getDomian($name, $region)
{
try {
$res = $this->app()->getBucketDomain($name, $region);
if ($res) {
$domainRules = $res->toArray()['ListBucketCustomDomainsResult'];
return array_column($domainRules, 'DomainName');
} else {
return [];
}
} catch (\Throwable $e) {
}
return [];
}
public function bindDomian(string $name, string $domain, string $region = null)
{
$parseDomin = parse_url($domain);
try {
$this->app()->putBucketDomain($name, $region, [
'domainname' => $parseDomin['host'],
]);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
public function setBucketCors(string $name, string $region)
{
try {
$this->app()->putBucketCors($name, $region, [
'AllowedHeader' => ['*'],
'AllowedMethod' => ['PUT', 'GET', 'POST', 'DELETE', 'HEAD'],
'AllowedOrigin' => ['*'],
'ExposeHeader' => ['ETag'],
'MaxAgeSeconds' => 0
]);
return true;
} catch (\Throwable $e) {
return $this->setError($e->getMessage());
}
}
/**
* @param string $callbackUrl
* @param string $dir
* @return array
* @throws \Exception
* @author 吴汐
* @email 442384644@qq.com
* @date 2023/06/19
*/
public function getTempKeys($callbackUrl = '', $dir = '')
{
// TODO: Implement getTempKeys() method.
$base64CallbackBody = base64_encode(json_encode([
'callbackUrl' => $callbackUrl,
'callbackBody' => 'filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}',
'callbackBodyType' => "application/x-www-form-urlencoded"
]));
$policy = json_encode([
'expiration' => $this->gmtIso8601(time() + 300),
'conditions' =>
[
[0 => 'content-length-range', 1 => 0, 2 => 1048576000],
['bucket' => $this->storageName],
[0 => 'starts-with', 1 => '$key', 2 => $dir],
]
]);
$base64Policy = base64_encode($policy);
$signature = base64_encode(hash_hmac('sha1', $base64Policy, $this->secretKey, true));
return [
'accessid' => $this->accessKey,
'host' => $this->uploadUrl,
'policy' => $base64Policy,
'signature' => $signature,
'expire' => time() + 30,
'callback' => $base64CallbackBody,
'cdn' => $this->cdn,
'type' => 'OBS'
];
}
/**
* 获取ISO时间格式
* @param $time
* @return string
* @throws \Exception
*/
protected function gmtIso8601($time): string
{
$dtStr = date("c", $time);
$myDateTime = new \DateTime($dtStr);
$expiration = $myDateTime->format(DateTimeInterface::ISO8601);
$pos = strpos($expiration, '+');
$expiration = substr($expiration, 0, $pos);
return $expiration . "Z";
}
/**
* 缩略图
* @param string $filePath
* @param string $fileName
* @param string $type
* @return array|mixed
*/
public function thumb(string $filePath = '', string $fileName = '', string $type = 'all')
{
$filePath = $this->getFilePath($filePath);
$data = ['big' => $filePath, 'mid' => $filePath, 'small' => $filePath];
$this->fileInfo->filePathBig = $this->fileInfo->filePathMid = $this->fileInfo->filePathSmall = $this->fileInfo->filePathWater = $filePath;
if ($filePath) {
$config = $this->thumbConfig;
foreach ($this->thumb as $v) {
if ($type == 'all' || $type == $v) {
$height = 'thumb_' . $v . '_height';
$width = 'thumb_' . $v . '_width';
$key = 'filePath' . ucfirst($v);
if (sys_config('image_thumbnail_status', 1) && isset($config[$height]) && isset($config[$width]) && $config[$height] && $config[$width]) {
$this->fileInfo->$key = $filePath . '?x-oss-process=image/resize,h_' . $config[$height] . ',w_' . $config[$width];
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
} else {
$this->fileInfo->$key = $this->water($this->fileInfo->$key);
$data[$v] = $this->fileInfo->$key;
}
}
}
}
return $data;
}
/**
* 水印
* @param string $filePath
* @return mixed|string
*/
public function water(string $filePath = '')
{
$filePath = $this->getFilePath($filePath);
$waterConfig = $this->waterConfig;
$waterPath = $filePath;
if ($waterConfig['image_watermark_status'] && $filePath) {
if (strpos($filePath, '?x-oss-process') === false) {
$filePath .= '?x-oss-process=image';
}
switch ($waterConfig['watermark_type']) {
case 1://图片
if (!$waterConfig['watermark_image']) {
throw new AdminException('请先配置水印图片');
}
$waterPath = $filePath .= '/watermark,image_' . base64_encode($waterConfig['watermark_image']) . ',t_' . $waterConfig['watermark_opacity'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
break;
case 2://文字
if (!$waterConfig['watermark_text']) {
throw new AdminException('请先配置水印文字');
}
$waterConfig['watermark_text_color'] = str_replace('#', '', $waterConfig['watermark_text_color']);
$waterPath = $filePath .= '/watermark,text_' . base64_encode($waterConfig['watermark_text']) . ',color_' . $waterConfig['watermark_text_color'] . ',size_' . $waterConfig['watermark_text_size'] . ',g_' . ($this->position[$waterConfig['watermark_position']] ?? 'nw') . ',x_' . $waterConfig['watermark_x'] . ',y_' . $waterConfig['watermark_y'];
break;
}
}
return $waterPath;
}
}

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);
}
}

Some files were not shown because too many files have changed in this diff Show More