init
- 框架初始化 - 安装插件 - 修复PHP8.4报错
This commit is contained in:
260
addons/shopro/library/Export.php
Normal file
260
addons/shopro/library/Export.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use Cache\Adapter\Redis\RedisCachePool;
|
||||
use Cache\Bridge\SimpleCache\SimpleCacheBridge;
|
||||
use PhpOffice\PhpSpreadsheet\Settings;
|
||||
use addons\shopro\library\Redis;
|
||||
|
||||
class Export
|
||||
{
|
||||
protected $last_memory_limit = '256M';
|
||||
|
||||
protected $config = [
|
||||
'save_type' => 'download', // 保存方式,download:直接下载, save:保存到服务器
|
||||
|
||||
'list_rows' => 1000, // 每次查询数据条数
|
||||
|
||||
'save_path' => RUNTIME_PATH . 'storage/export/', // 如果保存到服务器,保存路径
|
||||
|
||||
'memory_limit' => '512M', // php 进程内存限制(仅在导出过程生效), 注意单位 M
|
||||
|
||||
'time_limit' => 0, // php 超时时间,单位秒,0 不限制
|
||||
|
||||
'cache_driver' => 'default', // 数据临时存储驱动,default:内存, redis:redis
|
||||
|
||||
'redis_select' => null, // 导出大数据时,请尽量配置一个空的 redis db 库(导出失败,可以随时清除),以免导出失败,db 库中被存入大量垃圾数据
|
||||
];
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config['time_limit'] = intval($this->config['time_limit']);
|
||||
$this->config['list_rows'] = intval($this->config['list_rows']) ? intval($this->config['list_rows']) : 1000;
|
||||
|
||||
// 设置导出限制
|
||||
$this->setLimit();
|
||||
|
||||
// 设置导出时缓存
|
||||
$this->setCache();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 导出
|
||||
*
|
||||
* @param array $params
|
||||
* @param \Closure $callback
|
||||
* @return void
|
||||
*/
|
||||
public function export($params, \Closure $callback)
|
||||
{
|
||||
$fileName = $params['file_name']; // 文件名
|
||||
$cellTitles = $params['cell_titles']; // 标题
|
||||
$cell_num = count($cellTitles); // 标题数量
|
||||
$total = $params['total']; // 记录总条数
|
||||
$is_sub_cell = $params['is_sub_cell'] ?? false; // 是否有子数据
|
||||
$sub_start_cell = $params['sub_start_cell'] ?? null; // 子数据开始字段
|
||||
$sub_field = $params['sub_field'] ?? null; // 子数据字段名
|
||||
|
||||
$cellName = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ');
|
||||
$last_cell_key = $cellName[$cell_num - 1]; // 最后一列的标头
|
||||
// 最后一页
|
||||
$last_page = intval(ceil($total / $this->config['list_rows']));
|
||||
|
||||
// 实例化excel
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
// 初始化工作簿
|
||||
$sheet = $spreadsheet->getActiveSheet(0);
|
||||
|
||||
// 给表头字体加粗
|
||||
$sheet->getStyle('A1:' . $last_cell_key . '1')->getFont()->setBold(true);
|
||||
|
||||
// 表头
|
||||
$i = 0;
|
||||
foreach ($cellTitles as $key => $cell) {
|
||||
$sheet->setCellValue($cellName[$i] . '1', $cell);
|
||||
$i++;
|
||||
}
|
||||
|
||||
$cell_total = 2; // 当前表格已有行数
|
||||
for($page = 1;$page <= $last_page;$page++) {
|
||||
$is_last_page = $page == $last_page ? true : false;
|
||||
|
||||
// 获取数据
|
||||
$datas = $callback([
|
||||
'page' => $page,
|
||||
'list_rows' => $this->config['list_rows'],
|
||||
'is_last_page' => $is_last_page
|
||||
]);
|
||||
|
||||
foreach ($datas as $key => $data) {
|
||||
if ($is_last_page && $key == count($datas) - 1 && (!is_array($data) || count($data) == 1)) {
|
||||
$total_text = is_array($data) ? current($data) : $data;
|
||||
$sheet->mergeCells('A' . $cell_total . ':' . $last_cell_key . $cell_total);
|
||||
$sheet->setCellValue('A' . $cell_total, $total_text);
|
||||
} else {
|
||||
$items_count = 1;
|
||||
if ($is_sub_cell) {
|
||||
$items_count = count($data[$sub_field]);
|
||||
}
|
||||
$items_count = $items_count >= 1 ? $items_count : 1;
|
||||
// 每条记录设置边框
|
||||
// $sheet->getStyle('A' . ($cell_total).':' . $last_cell_key . ($cell_total + $items_count - 1))->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
|
||||
|
||||
$i = 0; // 当前循环到第几列了
|
||||
$sub_start = false;
|
||||
foreach ($cellTitles as $k => $cell) {
|
||||
if ($k == $sub_start_cell) {
|
||||
// 如果有子数据,是否循环到了子数据
|
||||
$sub_start = true;
|
||||
}
|
||||
|
||||
if ($is_sub_cell) {
|
||||
if (!$sub_start) {
|
||||
// 循环主数据
|
||||
$current_text = $data[$k] ?? '';
|
||||
if ($items_count > 1) {
|
||||
// items 有多个,需要合并单元格
|
||||
$sheet->mergeCells($cellName[$i] . ($cell_total) . ':' . $cellName[$i] . ($cell_total + $items_count - 1));
|
||||
$sheet->getCell($cellName[$i] . ($cell_total))->getStyle()->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
|
||||
}
|
||||
$sheet->setCellValue($cellName[$i] . ($cell_total), $current_text);
|
||||
} else {
|
||||
// 循环子数据
|
||||
foreach ($data[$sub_field] as $j => $sub) {
|
||||
$current_text = $sub[$k] ?? '';
|
||||
$sheet->setCellValue($cellName[$i] . ($cell_total + $j), $current_text);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$current_text = $data[$k] ?? '';
|
||||
$sheet->setCellValue($cellName[$i] . $cell_total, $current_text);
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
// 增加数据写入条数
|
||||
$cell_total = $cell_total + $items_count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// 设置表格边框
|
||||
$sheet->getStyle('A1:' . $last_cell_key . $cell_total)->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
|
||||
|
||||
// ini_set('memory_limit', '256M');
|
||||
return $this->output($spreadsheet, $fileName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 输出
|
||||
*
|
||||
* @param object $spreadsheet
|
||||
* @param string $fileName
|
||||
* @return void
|
||||
*/
|
||||
public function output($spreadsheet, $fileName)
|
||||
{
|
||||
$class_name = '\\PhpOffice\\PhpSpreadsheet\\Writer\\Xlsx';
|
||||
if (!class_exists($class_name)) {
|
||||
error_stop('文件输出格式不支持');
|
||||
}
|
||||
|
||||
if ($this->config['save_type'] == 'save') {
|
||||
// 初始化目录
|
||||
if (!is_dir($this->config['save_path'])) {
|
||||
@mkdir($this->config['save_path'], 0755, true);
|
||||
}
|
||||
|
||||
$save_file = $this->config['save_path'] . $fileName . '-' . date('YmdHis') . '.xlsx';
|
||||
|
||||
$result = [
|
||||
'file_path' => str_replace(ROOT_PATH, '项目目录/', $save_file)
|
||||
];
|
||||
} else {
|
||||
$save_file = $fileName . '-' . date('YmdHis') . '.xlsx';
|
||||
|
||||
$result = [
|
||||
'file_name' => $save_file
|
||||
];
|
||||
|
||||
ob_end_clean();
|
||||
header('pragma:public');
|
||||
header("Content-type:application/octet-stream; charset=utf-8; name=" . urlencode($save_file));
|
||||
header("Content-Disposition:attachment; filename=" . urlencode($save_file)); //attachment新窗口打印inline本窗口打印
|
||||
|
||||
$save_file = 'php://output';
|
||||
}
|
||||
|
||||
$writer = new $class_name($spreadsheet);
|
||||
$writer->save($save_file);
|
||||
|
||||
return $result;
|
||||
|
||||
// 修改为原始内存限制,会影响文件下载,暂时不修改回原来内存
|
||||
// ini_set('memory_limit', $this->last_memory_limit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置php 进程内存限制
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLimit()
|
||||
{
|
||||
// 不限时
|
||||
set_time_limit($this->config['time_limit']);
|
||||
// 根据需要调大内存限制
|
||||
$this->last_memory_limit = ini_get('memory_limit');
|
||||
|
||||
ini_set('memory_limit', $this->config['memory_limit']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置导出临时缓存
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCache()
|
||||
{
|
||||
// 设置缓存
|
||||
if ($this->config['cache_driver'] == 'redis') {
|
||||
// 将表格数据暂存 redis,可以降低 php 进程内存占用
|
||||
if (!class_exists(RedisCachePool::class)) {
|
||||
// 需要安装扩展包 composer require cache/simple-cache-bridge cache/redis-adapter
|
||||
error_stop('请安装扩展包:composer require cache/simple-cache-bridge cache/redis-adapter');
|
||||
}
|
||||
if (is_null($this->config['redis_select'])) {
|
||||
error_stop('请在 addons/shopro/library/Export.php 文件,defaultConfig 中配置 redis_select 库');
|
||||
}
|
||||
|
||||
$options = [
|
||||
'select' => $this->config['redis_select']
|
||||
];
|
||||
$redis = (new Redis($options))->getRedis(); // 不冲突
|
||||
$pool = new RedisCachePool($redis);
|
||||
$simpleCache = new SimpleCacheBridge($pool);
|
||||
|
||||
Settings::setCache($simpleCache);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
25
addons/shopro/library/Hook.php
Normal file
25
addons/shopro/library/Hook.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
class Hook
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static function register ($behaviors = []) {
|
||||
$default = require ROOT_PATH . 'addons/shopro/hooks.php';
|
||||
|
||||
$behaviors = array_merge($default, $behaviors);
|
||||
foreach ($behaviors as $tag => $behavior) {
|
||||
// 数组反转 保证最上面的行为优先级最高
|
||||
$behavior = array_reverse($behavior);
|
||||
foreach ($behavior as $be) {
|
||||
\think\Hook::add($tag, $be, true); // 所有行为都插入最前面
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
addons/shopro/library/HttpClient.php
Normal file
27
addons/shopro/library/HttpClient.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
|
||||
/**
|
||||
* 本 HttpClient 主要为了解决 yansongda\pay Http 必须继承 ClientInterface 问题(fa 框架的 GuzzleHttp\client 为 6.* 未继承 psr ClientInterface
|
||||
* 也可直接将本类当作 GuzzleHttp\Client 使用
|
||||
*/
|
||||
class HttpClient extends Client implements ClientInterface
|
||||
{
|
||||
|
||||
public function sendRequest(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
$options[RequestOptions::SYNCHRONOUS] = true;
|
||||
$options[RequestOptions::ALLOW_REDIRECTS] = false;
|
||||
$options[RequestOptions::HTTP_ERRORS] = false;
|
||||
|
||||
return $this->sendAsync($request, $options)->wait();
|
||||
}
|
||||
}
|
||||
86
addons/shopro/library/Operator.php
Normal file
86
addons/shopro/library/Operator.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use app\admin\model\Admin;
|
||||
use app\admin\model\User;
|
||||
use app\admin\controller\shopro\user\User as ShoproUser;
|
||||
use app\common\model\User as CommonUser;
|
||||
|
||||
class Operator
|
||||
{
|
||||
const OPER_TYPE = [
|
||||
'admin' => '管理员',
|
||||
'user' => '用户',
|
||||
'system' => '系统'
|
||||
];
|
||||
/**
|
||||
* 获取操作人
|
||||
*/
|
||||
public static function get($user = NULL)
|
||||
{
|
||||
if ($user === NULL) {
|
||||
// 自动获取操作人
|
||||
$user = self::getDefaultOper();
|
||||
}
|
||||
|
||||
if ($user instanceof Admin) {
|
||||
$oper = [
|
||||
'id' => $user->id,
|
||||
'name' => $user->nickname,
|
||||
'avatar' => $user->avatar,
|
||||
'type' => 'admin',
|
||||
'type_text' => (self::OPER_TYPE)['admin']
|
||||
];
|
||||
} elseif ($user instanceof User || $user instanceof ShoproUser || $user instanceof CommonUser) {
|
||||
$oper = [
|
||||
'id' => $user->id,
|
||||
'name' => $user->nickname,
|
||||
'avatar' => $user->avatar,
|
||||
'type' => 'user',
|
||||
'type_text' => (self::OPER_TYPE)['user']
|
||||
];
|
||||
} else {
|
||||
$oper = [
|
||||
'id' => 0,
|
||||
'name' => '',
|
||||
'avatar' => '',
|
||||
'type' => 'system',
|
||||
'type_text' => (self::OPER_TYPE)['system']
|
||||
];
|
||||
}
|
||||
return $oper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析操作人信息
|
||||
*/
|
||||
public static function info($type, $user = NULL)
|
||||
{
|
||||
return [
|
||||
'id' => $user['id'] ?? 0,
|
||||
'name' => $user['nickname'] ?? '',
|
||||
'avatar' => $user['avatar'] ?? '',
|
||||
'type' => $type,
|
||||
'type_text' => (self::OPER_TYPE)[$type]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认操作人
|
||||
*/
|
||||
private static function getDefaultOper()
|
||||
{
|
||||
$user = NULL;
|
||||
|
||||
if (!request()->isCli()) {
|
||||
// 检测管理员登陆
|
||||
$user = auth_admin();
|
||||
if (!$user) {
|
||||
// 检测用户登陆
|
||||
$user = auth_user();
|
||||
}
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
99
addons/shopro/library/Pipeline.php
Normal file
99
addons/shopro/library/Pipeline.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class Pipeline
|
||||
{
|
||||
protected $passable;
|
||||
|
||||
protected $pipes = [];
|
||||
|
||||
protected $exceptionHandler;
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
* @param $passable
|
||||
* @return $this
|
||||
*/
|
||||
public function send($passable)
|
||||
{
|
||||
$this->passable = $passable;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用栈
|
||||
* @param $pipes
|
||||
* @return $this
|
||||
*/
|
||||
public function through($pipes)
|
||||
{
|
||||
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行
|
||||
* @param Closure $destination
|
||||
* @return mixed
|
||||
*/
|
||||
public function then(Closure $destination)
|
||||
{
|
||||
$pipeline = array_reduce(
|
||||
array_reverse($this->pipes),
|
||||
$this->carry(),
|
||||
function ($passable) use ($destination) {
|
||||
try {
|
||||
return $destination($passable);
|
||||
} catch (Throwable | Exception $e) {
|
||||
return $this->handleException($passable, $e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return $pipeline($this->passable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异常处理器
|
||||
* @param callable $handler
|
||||
* @return $this
|
||||
*/
|
||||
public function whenException($handler)
|
||||
{
|
||||
$this->exceptionHandler = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function carry()
|
||||
{
|
||||
return function ($stack, $pipe) {
|
||||
return function ($passable) use ($stack, $pipe) {
|
||||
try {
|
||||
return $pipe($passable, $stack);
|
||||
} catch (Throwable | Exception $e) {
|
||||
return $this->handleException($passable, $e);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常处理
|
||||
* @param $passable
|
||||
* @param $e
|
||||
* @return mixed
|
||||
*/
|
||||
protected function handleException($passable, Throwable $e)
|
||||
{
|
||||
if ($this->exceptionHandler) {
|
||||
return call_user_func($this->exceptionHandler, $passable, $e);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
}
|
||||
73
addons/shopro/library/Redis.php
Normal file
73
addons/shopro/library/Redis.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use Redis as SystemRedis;
|
||||
|
||||
class Redis
|
||||
{
|
||||
protected $handler = null;
|
||||
|
||||
protected $options = [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'password' => '',
|
||||
'select' => 0,
|
||||
'timeout' => 0,
|
||||
'expire' => 0,
|
||||
'persistent' => false,
|
||||
'prefix' => '',
|
||||
];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param array $options 缓存参数
|
||||
* @access public
|
||||
*/
|
||||
public function __construct($options = [])
|
||||
{
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new \BadFunctionCallException('not support: redis');
|
||||
}
|
||||
// 获取 redis 配置
|
||||
$config = config('redis');
|
||||
$config = $config ?: [];
|
||||
$this->options = array_merge($this->options, $config);
|
||||
|
||||
if (!empty($options)) {
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
$this->handler = new SystemRedis();
|
||||
if ($this->options['persistent']) {
|
||||
$this->handler->pconnect($this->options['host'], intval($this->options['port']), $this->options['timeout'], 'persistent_id_' . $this->options['select']);
|
||||
} else {
|
||||
$this->handler->connect($this->options['host'], intval($this->options['port']), $this->options['timeout']);
|
||||
}
|
||||
|
||||
if ('' != $this->options['password']) {
|
||||
$this->handler->auth($this->options['password']);
|
||||
}
|
||||
|
||||
if (0 != $this->options['select']) {
|
||||
$this->handler->select(intval($this->options['select']));
|
||||
}
|
||||
}
|
||||
|
||||
public function getRedis()
|
||||
{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 方法转发到redis
|
||||
*
|
||||
* @param string $funcname
|
||||
* @param array $arguments
|
||||
* @return void
|
||||
*/
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
return $this->getRedis()->{$funcname}(...$arguments);
|
||||
}
|
||||
}
|
||||
133
addons/shopro/library/RedisCache.php
Normal file
133
addons/shopro/library/RedisCache.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use addons\shopro\facade\Redis as RedisFacade;
|
||||
|
||||
class RedisCache
|
||||
{
|
||||
|
||||
protected $redis = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function get($key, $default = null)
|
||||
{
|
||||
if (RedisFacade::exists($key)) {
|
||||
$value = RedisFacade::get($key);
|
||||
return !is_null($value) ? $this->unserialize($value) : null;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
|
||||
public function set($key, $value, $ttl = null)
|
||||
{
|
||||
$value = $this->serialize($value);
|
||||
|
||||
if ($ttl) {
|
||||
$result = RedisFacade::setex($key, $ttl, $value);
|
||||
} else {
|
||||
$result = RedisFacade::set($key, $value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 判断一个项目在缓存中是否存在
|
||||
* @param string $key 缓存键值
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($key)
|
||||
{
|
||||
return RedisFacade::exists($key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除指定键值的缓存项
|
||||
*
|
||||
* @param string $key 指定的唯一缓存key对应的项目将会被删除
|
||||
*
|
||||
* @return bool 成功删除时返回ture,有其它错误时时返回false
|
||||
*/
|
||||
public function delete($key)
|
||||
{
|
||||
return RedisFacade::del($key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 单次操作删除多个缓存项目.
|
||||
*
|
||||
* @param iterable $keys 一个基于字符串键列表会被删除
|
||||
*
|
||||
* @return bool True 所有项目都成功被删除时回true,有任何错误时返回false
|
||||
*/
|
||||
public function deleteMultiple($keys) {}
|
||||
|
||||
|
||||
/**
|
||||
* 清除所有缓存中的key
|
||||
*
|
||||
* @return bool 成功返回True.失败返回False
|
||||
*/
|
||||
public function clear() {}
|
||||
|
||||
/**
|
||||
* 根据指定的缓存键值列表获取得多个缓存项目
|
||||
*
|
||||
* @param iterable $keys 在单次操作中可被获取的键值项
|
||||
* @param mixed $default 如果key不存在时,返回的默认值
|
||||
*
|
||||
* @return iterable 返回键值对(key=>value形式)列表。如果key不存在,或者已经过期时,返回默认值。
|
||||
*/
|
||||
public function getMultiple($keys, $default = null) {}
|
||||
|
||||
/**
|
||||
* 存储一个键值对形式的集合到缓存中。
|
||||
*
|
||||
* @param iterable $values 一系列操作的键值对列表
|
||||
* @param null|int|\DateInterval $ttl 可选项.项目的存在时间,如果该值没有设置,且驱动支持生存时间时,将设置一个默认值,或者驱自行处理。
|
||||
*
|
||||
* @return bool 成功返回True.失败返回False.
|
||||
*/
|
||||
public function setMultiple($values, $ttl = null) {}
|
||||
|
||||
|
||||
/**
|
||||
* Serialize the value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function serialize($value)
|
||||
{
|
||||
return is_numeric($value) && !in_array($value, [INF, -INF]) && !is_nan($value) ? $value : serialize($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserialize the value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function unserialize($value)
|
||||
{
|
||||
return is_numeric($value) ? $value : unserialize($value);
|
||||
}
|
||||
|
||||
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
return RedisFacade::instance()->{$funcname}(...$arguments);
|
||||
}
|
||||
|
||||
}
|
||||
230
addons/shopro/library/Tree.php
Normal file
230
addons/shopro/library/Tree.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use think\model\Collection;
|
||||
|
||||
class Tree
|
||||
{
|
||||
protected $model = null;
|
||||
|
||||
public function __construct($model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取递归树
|
||||
*
|
||||
* @param integer|array|Collection $items 可以传入某个id 查询这个id的下级树,也可以传一个查询列表结果,将获取这个列表的所有的下级 树
|
||||
* @param \Closure $resultCb 用来处理每一次查询的结果 比如要取出树中的所有 name
|
||||
* @return Collection
|
||||
*/
|
||||
public function getTree($items = 0, \Closure $resultCb = null)
|
||||
{
|
||||
if (!is_array($items) && !$items instanceof Collection) {
|
||||
$items = $this->getQuery()->where('parent_id', $items)->select();
|
||||
$resultCb && $items = $resultCb($items);
|
||||
}
|
||||
|
||||
foreach ($items as $key => &$item) {
|
||||
$child = $this->getTree($item->id, $resultCb);
|
||||
if ($child) {
|
||||
$item->children = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取递归树(包含自身)
|
||||
*
|
||||
* @param integer $id
|
||||
* @param \Closure $resultCb 用来处理每一次查询的结果 比如要取出树中的所有 name
|
||||
* @return Collection
|
||||
*/
|
||||
public function getChildren($id, \Closure $resultCb = null)
|
||||
{
|
||||
$self = $this->getQuery()->where('id', $id)->select();
|
||||
if(!$self) {
|
||||
error_stop('未找到数据');
|
||||
}
|
||||
|
||||
$items = $this->getQuery()->where('parent_id', $id)->select();
|
||||
$resultCb && $items = $resultCb($items);
|
||||
|
||||
foreach ($items as $key => &$item) {
|
||||
$child = $this->getTree($item->id, $resultCb);
|
||||
if ($child) {
|
||||
$item->children = $child;
|
||||
}
|
||||
}
|
||||
$self[0]->children = $items;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检测id 是不是自己的下级
|
||||
*
|
||||
* @param [type] $parent_id
|
||||
* @param [type] $id
|
||||
* @return void
|
||||
*/
|
||||
public function checkParent($parent_id, $id)
|
||||
{
|
||||
if ($parent_id == $id) {
|
||||
error_stop('当前上级不能是自己');
|
||||
}
|
||||
$childIds = $this->getChildIds($id);
|
||||
if (in_array($parent_id, $childIds)) {
|
||||
error_stop('当前上级不能是自己的下级');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前对象所属级别
|
||||
*
|
||||
* @param [type] $object
|
||||
* @return void
|
||||
*/
|
||||
public function getLevel($object)
|
||||
{
|
||||
$parentIds = $this->getParentFields($object, 'id');
|
||||
return count($parentIds);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 缓存递归获取当前对象的上级 指定字段
|
||||
*
|
||||
* @param \think\Model|int $id
|
||||
* @param boolean $self 是否包含自己
|
||||
* @return array
|
||||
*/
|
||||
public function getParentFields($item, $field = 'id', $self = true)
|
||||
{
|
||||
if (!$item instanceof \think\Model) {
|
||||
$item = $this->getQuery()->find($item);
|
||||
if (!$item) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// 判断缓存
|
||||
$cacheKey = 'object-' . $this->getTable() . '-' . $item->id . '-' . $field . '-parent-ids';
|
||||
$objectIds = cache($cacheKey);
|
||||
|
||||
if (!$objectIds) {
|
||||
$objectIds = array_reverse($this->recursionGetParentFields($item, $field));
|
||||
if ($self) {
|
||||
$objectIds[] = $item[$field]; // 加上自己
|
||||
}
|
||||
// 缓存暂时注释,如果需要,可以打开,请注意后台更新角色记得清除缓存
|
||||
// cache($cacheKey, $objectIds, (600 + mt_rand(0, 300))); // 加入随机秒数,防止一起全部过期
|
||||
}
|
||||
|
||||
return $objectIds;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 递归获取所有上级 id
|
||||
*/
|
||||
private function recursionGetParentFields($item, $field = 'id', $ids = [])
|
||||
{
|
||||
if ($item->parent_id) {
|
||||
$parent = $this->getQuery()->find($item->parent_id);
|
||||
if ($parent) {
|
||||
$ids[] = $parent[$field];
|
||||
return $this->recursionGetParentFields($parent, $field, $ids);
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 缓存递归获取子对象 id
|
||||
*
|
||||
* @param int $id 要查询的 id
|
||||
* @param boolean $self 是否包含自己
|
||||
* @return array
|
||||
*/
|
||||
public function getChildIds($id, $self = true)
|
||||
{
|
||||
// 判断缓存
|
||||
$cacheKey = 'object-' . $this->getTable() . '-' . $id . '-child-ids';
|
||||
$objectIds = cache($cacheKey);
|
||||
|
||||
if (!$objectIds) {
|
||||
$objectIds = $this->recursionGetChildIds($id, $self);
|
||||
|
||||
// 缓存暂时注释,如果需要,可以打开,请注意后台更新角色记得清除缓存
|
||||
// cache($cacheKey, $objectIds, (600 + mt_rand(0, 300))); // 加入随机秒数,防止一起全部过期
|
||||
}
|
||||
|
||||
return $objectIds;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 递归获取子分类 id
|
||||
*
|
||||
*/
|
||||
private function recursionGetChildIds($id, $self)
|
||||
{
|
||||
$ids = $self ? [$id] : [];
|
||||
$childrenIds = $this->getQuery()->where(['parent_id' => $id])->column('id');
|
||||
|
||||
if ($childrenIds) {
|
||||
foreach ($childrenIds as $v) {
|
||||
$grandsonIds = $this->recursionGetChildIds($v, true);
|
||||
$ids = array_merge($ids, $grandsonIds);
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取当前 查询
|
||||
*
|
||||
* @return think\model|think\db\Query
|
||||
*/
|
||||
private function getQuery()
|
||||
{
|
||||
if ($this->model instanceof \Closure) {
|
||||
return ($this->model)();
|
||||
}
|
||||
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取表
|
||||
*/
|
||||
private function getTable()
|
||||
{
|
||||
$query = $this->getQuery();
|
||||
if ($query instanceof \think\Model) {
|
||||
$table_name = $query->getQuery()->getTable();
|
||||
} else {
|
||||
$table_name = $query->getTable();
|
||||
}
|
||||
|
||||
return $table_name;
|
||||
}
|
||||
}
|
||||
40
addons/shopro/library/Websocket.php
Normal file
40
addons/shopro/library/Websocket.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library;
|
||||
|
||||
use fast\Http;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class Websocket
|
||||
{
|
||||
use Helper;
|
||||
|
||||
protected $config = null;
|
||||
|
||||
protected $base_uri = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = $this->getConfig('system');
|
||||
$inside_host = $this->config['inside_host'] ?? '127.0.0.1';
|
||||
$inside_port = $this->config['inside_port'] ?? '9191';
|
||||
|
||||
$this->base_uri = 'http://' . $inside_host . ':' . $inside_port;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function notification($data)
|
||||
{
|
||||
$client = new Client();
|
||||
$response = $client->post($this->base_uri . '/notification', [
|
||||
'form_params' => $data
|
||||
]);
|
||||
|
||||
// 获取结果
|
||||
$result = $response->getBody()->getContents();
|
||||
|
||||
return $result == 'ok' ? true : $result;
|
||||
}
|
||||
}
|
||||
379
addons/shopro/library/activity/Activity.php
Normal file
379
addons/shopro/library/activity/Activity.php
Normal file
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity;
|
||||
|
||||
use addons\shopro\facade\ActivityRedis;
|
||||
use addons\shopro\library\activity\contract\ActivityInterface;
|
||||
use addons\shopro\library\activity\contract\ActivityGetterInterface;
|
||||
|
||||
class Activity
|
||||
{
|
||||
/**
|
||||
* 活动model
|
||||
*/
|
||||
public $model = null;
|
||||
public $redis = null;
|
||||
|
||||
protected $type = null;
|
||||
protected $rules = null;
|
||||
|
||||
protected $hasRedis = null;
|
||||
|
||||
protected $getters = [];
|
||||
|
||||
public function __construct($model_name)
|
||||
{
|
||||
$this->hasRedis = has_redis();
|
||||
$this->model = new $model_name;
|
||||
$this->redis = ActivityRedis::instance();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加活动
|
||||
*
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
public function save($params)
|
||||
{
|
||||
$this->rules = $params['rules'];
|
||||
$this->type = $params['type'];
|
||||
$params['classify'] = $this->model->getClassify($this->type); // 设置 classify
|
||||
$params['prehead_time'] = in_array($params['classify'], ['promo', 'app']) ? '' : ($params['prehead_time'] ?? ''); // 触发触发器,promo 不能设置 prehead_time
|
||||
// 检测活动之间的冲突
|
||||
$this->checkActivity($params);
|
||||
|
||||
// 保存活动
|
||||
$this->model->allowField(true)->save($params);
|
||||
|
||||
// 保存活动其他数据
|
||||
$this->saveOther($params);
|
||||
|
||||
if ($this->hasRedis) {
|
||||
$this->redis->setActivity($this->model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新活动
|
||||
*
|
||||
* @param \think\Model $activity
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
public function update($activity, $params)
|
||||
{
|
||||
$this->model = $activity;
|
||||
|
||||
$this->rules = $params['rules'];
|
||||
$this->type = $activity->type;
|
||||
$params['type'] = $activity->type; // 活动类型不可编辑,赋值活动本身的 type
|
||||
$params['classify'] = $this->model->getClassify($this->type); // 设置 classify
|
||||
$params['prehead_time'] = in_array($params['classify'], ['promo', 'app']) ? '' : ($params['prehead_time'] ?? ''); // 触发触发器,promo 不能设置 prehead_time
|
||||
|
||||
if ($activity->status == 'ended') {
|
||||
error_stop('活动已结束');
|
||||
}
|
||||
|
||||
// 检测活动之间的冲突
|
||||
$params = $this->checkActivity($params, $this->model->id);
|
||||
|
||||
$activities = $activity->classifies()['activity'];
|
||||
$activities = array_keys($activities);
|
||||
if ($activity->status == 'ing') {
|
||||
if (in_array($activity->type, $activities)) {
|
||||
// 活动正在进行中,只能改结束时间
|
||||
$params = [
|
||||
'title' => $params['title'],
|
||||
'end_time' => $params['end_time'],
|
||||
'goods_list' => $params['goods_list'],
|
||||
'richtext_id' => $params['richtext_id'],
|
||||
'richtext_title' => $params['richtext_title'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 保存活动
|
||||
$this->model->allowField(true)->save($params);
|
||||
|
||||
// 保存活动其他数据
|
||||
$this->saveOther($params);
|
||||
|
||||
if ($this->hasRedis) {
|
||||
$this->redis->setActivity($this->model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除活动
|
||||
*
|
||||
* @param \think\Model $activity
|
||||
* @return void
|
||||
*/
|
||||
public function delete($activity)
|
||||
{
|
||||
if ($this->hasRedis) {
|
||||
$this->redis->delActivity($activity);
|
||||
}
|
||||
|
||||
return $activity->delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活动规格相关数据展示
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $rules
|
||||
* @return array
|
||||
*/
|
||||
public function showSkuPrice($type, $skuPrice)
|
||||
{
|
||||
$skuPrice = $this->provider($type)->showSkuPrice($skuPrice);
|
||||
|
||||
return $skuPrice;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活动规则相关信息
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $rules
|
||||
* @return array
|
||||
*/
|
||||
public function rulesInfo($type, $rules)
|
||||
{
|
||||
$this->rules = $rules;
|
||||
$this->type = $type;
|
||||
|
||||
$activity = $this->provider()->rulesInfo($type, $rules);
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验活动特有的数据
|
||||
*
|
||||
* @param array $params
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function checkActivity($params, $activity_id = 0, $type = null)
|
||||
{
|
||||
return $this->provider($type)->check($params, $activity_id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存活动特有的数据
|
||||
*
|
||||
* @param array $params
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function saveOther($params, $type = null)
|
||||
{
|
||||
return $this->provider($type)->save($this->model, $params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化促销标签
|
||||
*
|
||||
* @param array $rules
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function formatRuleTags($rules, $type = null)
|
||||
{
|
||||
return $this->provider($type)->formatTags($rules, $type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化促销标签
|
||||
*
|
||||
* @param array $rules
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function formatRuleTexts($rules, $type = null)
|
||||
{
|
||||
return $this->provider($type)->formatTexts($rules, $type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用活动覆盖商品数据
|
||||
*
|
||||
* @param \think\Model|array $goods
|
||||
* @return void
|
||||
*/
|
||||
public function recoverSkuPrices($goods, $activity)
|
||||
{
|
||||
$skuPrices = $this->provider($activity['type'])->recoverSkuPrices($goods, $activity);
|
||||
return $skuPrices;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活动购买检测(仅处理活动,不处理促销)
|
||||
*
|
||||
* @param array $buyInfo
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function buyCheck($buyInfo, $activity)
|
||||
{
|
||||
if ($activity) {
|
||||
return $this->provider($activity['type'])->buyCheck($buyInfo, $activity);
|
||||
}
|
||||
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 活动购买检测(仅处理活动,不处理促销)
|
||||
*
|
||||
* @param array $buyInfo
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function buy($buyInfo, $activity)
|
||||
{
|
||||
if ($activity) {
|
||||
return $this->provider($activity['type'])->buy($buyInfo, $activity);
|
||||
}
|
||||
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 购买成功
|
||||
*
|
||||
* @param array|object $order
|
||||
* @param array|object $user
|
||||
* @return array
|
||||
*/
|
||||
public function buyOk($order, $user)
|
||||
{
|
||||
if ($order->activity_type) {
|
||||
$this->provider($order->activity_type)->buyOk($order, $user);
|
||||
}
|
||||
|
||||
if ($order->promo_types) {
|
||||
$promoTypes = explode(',', $order->promo_types);
|
||||
foreach ($promoTypes as $promo_type) {
|
||||
$this->provider($promo_type)->buyOk($order, $user);
|
||||
}
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 购买失败(释放库存,剪掉销量,移除参团数据)
|
||||
*
|
||||
* @param array|object $order
|
||||
* @param string $type 失败类型:invalid=订单取消,关闭;refund=退款
|
||||
* @return array
|
||||
*/
|
||||
public function buyFail($order, $type)
|
||||
{
|
||||
if ($order->activity_type) {
|
||||
$this->provider($order->activity_type)->buyFail($order, $type);
|
||||
}
|
||||
|
||||
if ($order->promo_types) {
|
||||
$promoTypes = explode(',', $order->promo_types);
|
||||
foreach ($promoTypes as $promo_type) {
|
||||
$this->provider($promo_type)->buyFail($order, $type);
|
||||
}
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取促销优惠信息
|
||||
*
|
||||
* @param array $promo
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function getPromoInfo($promo, ?array $data = [])
|
||||
{
|
||||
return $this->provider($promo['type'])->getPromoInfo($promo, $data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 活动提供器
|
||||
*
|
||||
* @param string $type
|
||||
* @return ActivityInterface
|
||||
*/
|
||||
public function provider($type = null)
|
||||
{
|
||||
$type = $type ?: $this->type;
|
||||
$class = "\\addons\\shopro\\library\\activity\\provider\\" . \think\helper\Str::studly($type);
|
||||
if (class_exists($class)) {
|
||||
return new $class($this);
|
||||
}
|
||||
|
||||
error_stop('活动类型不支持');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取活动提供器
|
||||
*
|
||||
* @param string $getter
|
||||
* @return ActivityGetterInterface
|
||||
*/
|
||||
public function getter($getter = null)
|
||||
{
|
||||
$getter = $getter ? $getter : $this->defaultGetter();
|
||||
|
||||
if (isset($this->getters[$getter])) {
|
||||
return $this->getters[$getter];
|
||||
}
|
||||
|
||||
$class = "\\addons\\shopro\\library\\activity\\getter\\" . \think\helper\Str::studly($getter);
|
||||
if (class_exists($class)) {
|
||||
return $this->getters[$getter] = new $class($this);
|
||||
}
|
||||
|
||||
error_stop('活动类型不支持');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取默认获取器
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function defaultGetter()
|
||||
{
|
||||
return $this->hasRedis ? 'redis' : 'db';
|
||||
}
|
||||
|
||||
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
return $this->getter()->{$funcname}(...$arguments);
|
||||
}
|
||||
}
|
||||
186
addons/shopro/library/activity/ActivityRedis.php
Normal file
186
addons/shopro/library/activity/ActivityRedis.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity;
|
||||
|
||||
use addons\shopro\facade\Redis;
|
||||
use app\admin\model\shopro\activity\Activity as ActivityModel;
|
||||
use addons\shopro\library\activity\traits\ActivityRedis as ActivityRedisTrait;
|
||||
|
||||
class ActivityRedis
|
||||
{
|
||||
use ActivityRedisTrait;
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将活动设置到 redis 中
|
||||
*
|
||||
* @param mixed $activity
|
||||
* @param array $goodsList
|
||||
* @return void
|
||||
*/
|
||||
public function setActivity($activity)
|
||||
{
|
||||
$activity = ActivityModel::with('activity_sku_prices')->where('id', $activity['id'])->find();
|
||||
|
||||
// hash 键值
|
||||
$keyActivity = $this->keyActivity($activity->id, $activity->type);
|
||||
|
||||
// 删除旧的可变数据,需要排除销量 key
|
||||
if (Redis::EXISTS($keyActivity)) {
|
||||
// 如果 hashKey 存在,删除规格
|
||||
$activityCache = $this->getActivityByKey($keyActivity);
|
||||
|
||||
foreach ($activityCache as $field => $value) {
|
||||
// 是商品规格,并且不是销量
|
||||
if (strpos($field, $this->hashGoodsPrefix) !== false && strpos($field, '-sale') === false) {
|
||||
// 商品规格信息,删掉
|
||||
Redis::HDEL($keyActivity, $field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Redis::HMSET(
|
||||
$keyActivity,
|
||||
[
|
||||
'id' => $activity['id'],
|
||||
'title' => $activity['title'],
|
||||
'type' => $activity['type'],
|
||||
'type_text' => $activity['type_text'],
|
||||
'classify' => $activity['classify'],
|
||||
'goods_ids' => $activity['goods_ids'],
|
||||
'richtext_id' => $activity['richtext_id'],
|
||||
'richtext_title' => $activity['richtext_title'],
|
||||
'prehead_time' => strtotime($activity['prehead_time']),
|
||||
'start_time' => strtotime($activity['start_time']),
|
||||
'end_time' => strtotime($activity['end_time']),
|
||||
'rules' => is_array($activity['rules']) ? json_encode($activity['rules']) : $activity['rules'],
|
||||
]
|
||||
);
|
||||
|
||||
// 将活动规格保存 redis
|
||||
foreach ($activity['activity_sku_prices'] as $goods) {
|
||||
unset($goods['sales']); // 规格销量单独字段保存 goods-id-id-sale key
|
||||
$keyActivityGoods = $this->keyActivityGoods($goods['goods_id'], $goods['goods_sku_price_id']);
|
||||
// 获取当前规格的销量,修改库存的时候,需要把 stock 加上这部分销量
|
||||
$cacheSale = Redis::HGET($keyActivity, $keyActivityGoods . '-sale');
|
||||
$goods['stock'] = $goods['stock'] + $cacheSale;
|
||||
Redis::HSET($keyActivity, $keyActivityGoods, json_encode($goods));
|
||||
}
|
||||
|
||||
// 将 hash 键值存入 有序集合,score 为 id
|
||||
Redis::ZADD($this->zsetKey, strtotime($activity['start_time']), $keyActivity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 删除活动缓存
|
||||
*
|
||||
* @param object $activity
|
||||
* @return void
|
||||
*/
|
||||
public function delActivity($activity)
|
||||
{
|
||||
// hash 键值
|
||||
$keyActivity = $this->keyActivity($activity->id, $activity->type);
|
||||
|
||||
// 删除 hash
|
||||
Redis::DEL($keyActivity);
|
||||
|
||||
// 删除集合
|
||||
Redis::ZREM($this->zsetKey, $keyActivity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据活动类型,获取所有活动(前端:秒杀商品列表,拼团商品列表)
|
||||
*
|
||||
* @param array $activityTypes 要查询的活动类型
|
||||
* @param array|string $status 要查询的活动的状态
|
||||
* @param string $format_type // 格式化类型,默认clear,清理多余的字段,比如拼团的 团信息 normal=格式化拼团,秒杀等|promo=格式化满减,满折,赠送
|
||||
* @return array
|
||||
*/
|
||||
public function getActivityList($activityTypes = [], $status = 'all', $format_type = 'normal')
|
||||
{
|
||||
// 获取对应的活动类型的集合
|
||||
$keysActivity = $this->getKeysActivityByTypes($activityTypes, $status);
|
||||
|
||||
$activityList = [];
|
||||
foreach ($keysActivity as $keyActivity) {
|
||||
// 格式化活动
|
||||
$activity = $this->formatActivityByKey($keyActivity, $format_type);
|
||||
if ($activity) {
|
||||
$activityList[] = $activity;
|
||||
}
|
||||
}
|
||||
|
||||
return $activityList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询商品列表,详情时,获取这个商品对应的秒杀拼团等活动
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @param Array $activityTypes
|
||||
* @param array|string $status 要查询的活动的状态
|
||||
* @param string $format_type
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsActivitys($goods_id, $activityTypes = [], $status = 'all', $format_type = 'goods')
|
||||
{
|
||||
// 获取商品第一条活动的 hash key
|
||||
$keysActivity = $this->getkeysActivityByGoods($goods_id, $activityTypes, $status);
|
||||
|
||||
// 如果存在活动
|
||||
foreach ($keysActivity as $keyActivity) {
|
||||
// 格式化活动
|
||||
$activity = $this->formatActivityByKey($keyActivity, $format_type, ['goods_id' => $goods_id]);
|
||||
if ($activity) {
|
||||
$activityList[] = $activity;
|
||||
}
|
||||
}
|
||||
|
||||
return $activityList ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是商品的特定的活动
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @param integer $activity_id
|
||||
* @param array|string $status 要查询的活动的状态
|
||||
* @param string $format_type
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsActivityByActivity($goods_id, $activity_id, $status = 'all', $format_type = 'goods')
|
||||
{
|
||||
// 获取商品第一条活动的 hash key
|
||||
$keyActivity = $this->getKeyActivityById($activity_id);
|
||||
if (!$keyActivity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$activity = $this->formatActivityByKey($keyActivity, $format_type, ['goods_id' => $goods_id]);
|
||||
if ($activity) {
|
||||
// 判断商品
|
||||
$goods_ids = array_values(array_filter(explode(',', $activity['goods_ids'])));
|
||||
if (!in_array($goods_id, $goods_ids) && !empty($goods_ids)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断状态
|
||||
$status = is_array($status) ? $status : [$status];
|
||||
if (!in_array('all', $status)) {
|
||||
if (!in_array($activity['status'], $status)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $activity ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\contract;
|
||||
|
||||
interface ActivityGetterInterface
|
||||
{
|
||||
/**
|
||||
* 获取所有给定类型给定状态的活动
|
||||
*
|
||||
* @param array $activityTypes
|
||||
* @return array
|
||||
*/
|
||||
public function getActivities($activityTypes, $status = 'all');
|
||||
|
||||
|
||||
/**
|
||||
* 获取时间区间内的所有给定类型的活动
|
||||
*
|
||||
* @param array $range
|
||||
* @param array $activityTypes
|
||||
* @param string $range_type overlap=只要区间有重叠的就算|contain=包含,必须在这个区间之内的
|
||||
* @return array
|
||||
*/
|
||||
public function getActivitiesByRange($range, $activityTypes = [], $range_type = 'overlap');
|
||||
|
||||
|
||||
/**
|
||||
* 获取商品的所有正在进行,或正在预售的活动
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsActivitys($goods_id);
|
||||
|
||||
/**
|
||||
* 获取商品的所有正在进行,或正在预售的营销
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsPromos($goods_id);
|
||||
|
||||
|
||||
|
||||
}
|
||||
141
addons/shopro/library/activity/contract/ActivityInterface.php
Normal file
141
addons/shopro/library/activity/contract/ActivityInterface.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\contract;
|
||||
|
||||
interface ActivityInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* 活动规则表单验证
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function validate(array $data);
|
||||
|
||||
/**
|
||||
* 检查要添加的活动状态
|
||||
*
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
public function check(array $params);
|
||||
|
||||
|
||||
/**
|
||||
* 获取活动相关的信息
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $rules
|
||||
* @return array
|
||||
*/
|
||||
public function rulesInfo($type, $rules);
|
||||
|
||||
|
||||
/**
|
||||
* 保存当前活动的专属数据
|
||||
*
|
||||
* @param \think\Model $activity
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function save(\think\Model $activity, ?array $params = []);
|
||||
|
||||
/**
|
||||
* 展示当前活动规格的专属数据
|
||||
*
|
||||
* @param \think\Model $activity
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function showSkuPrice(\think\Model $skuPrice);
|
||||
|
||||
/**
|
||||
* 格式化活动标签(满减,满折等)
|
||||
*
|
||||
* @param array $rules
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function formatTags(array $rules, $type);
|
||||
|
||||
|
||||
/**
|
||||
* 格式化活动标签单个(满减,满折等)
|
||||
*
|
||||
* @param array $rules
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public function formatTag(array $discountData);
|
||||
|
||||
|
||||
/**
|
||||
* 格式化活动规则,完整版(满减,满折等)
|
||||
*
|
||||
* @param array $rules
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function formatTexts(array $rules, $type);
|
||||
|
||||
|
||||
/**
|
||||
* 覆盖商品活动数据
|
||||
*
|
||||
* @param \think\Model|array $goods
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function recoverSkuPrices(array $goods, ?array $activity);
|
||||
|
||||
|
||||
/**
|
||||
* 购买活动处理
|
||||
*
|
||||
* @param array $buyInfo
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function buyCheck($buyInfo, $activity);
|
||||
|
||||
|
||||
/**
|
||||
* 购买活动
|
||||
*
|
||||
* @param array $buyInfo
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function buy($buyInfo, $activity);
|
||||
|
||||
|
||||
/**
|
||||
* 购买活动成功
|
||||
*
|
||||
* @param array|object $order
|
||||
* @param array|object $user
|
||||
* @return array
|
||||
*/
|
||||
public function buyOk($order, $user);
|
||||
|
||||
|
||||
/**
|
||||
* 购买活动失败
|
||||
*
|
||||
* @param array|object $order
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function buyFail($order, $type);
|
||||
|
||||
|
||||
/**
|
||||
* 活动信息
|
||||
*
|
||||
* @param array $promo
|
||||
* @param array $data 附加数据
|
||||
* @return array
|
||||
*/
|
||||
public function getPromoInfo(array $promo, ?array $data = []);
|
||||
}
|
||||
20
addons/shopro/library/activity/getter/Base.php
Normal file
20
addons/shopro/library/activity/getter/Base.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\getter;
|
||||
|
||||
use addons\shopro\library\activity\contract\ActivityGetterInterface;
|
||||
use addons\shopro\library\activity\Activity as ActivityManager;
|
||||
|
||||
abstract class Base implements ActivityGetterInterface
|
||||
{
|
||||
protected $manager = null;
|
||||
protected $model = null;
|
||||
|
||||
public function __construct(ActivityManager $activityManager)
|
||||
{
|
||||
$this->manager = $activityManager;
|
||||
|
||||
$this->model = $activityManager->model;
|
||||
$this->redis = $activityManager->redis;
|
||||
}
|
||||
}
|
||||
138
addons/shopro/library/activity/getter/Db.php
Normal file
138
addons/shopro/library/activity/getter/Db.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\getter;
|
||||
|
||||
class Db extends Base
|
||||
{
|
||||
|
||||
/**
|
||||
* 获取所有给定类型给定状态的活动
|
||||
*
|
||||
* @param array $activityTypes
|
||||
* @return array
|
||||
*/
|
||||
public function getActivities($activityTypes, $status = 'all')
|
||||
{
|
||||
$status = is_array($status) ? $status : [$status];
|
||||
|
||||
$activities = $this->model->where('type', 'in', $activityTypes);
|
||||
|
||||
if (!in_array('all', $status)) {
|
||||
$activities = $activities->statusComb($status);
|
||||
}
|
||||
|
||||
$activities = $activities->select();
|
||||
return $activities;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定时间区间内的活动
|
||||
*
|
||||
* @param array $range
|
||||
* @param array $activityTypes
|
||||
* @param string $range_type overlap=只要区间有重叠的就算|contain=包含,必须在这个区间之内的
|
||||
* @return array
|
||||
*/
|
||||
public function getActivitiesByRange($range, $activityTypes = [], $range_type = 'overlap')
|
||||
{
|
||||
$activities = $this->model->where('type', 'in', $activityTypes);
|
||||
|
||||
if ($range_type == 'overlap') {
|
||||
$activities = $activities->where('prehead_time', '<=', $range[1])->where('end_time', '>=', $range[0]);
|
||||
} elseif ($range_type == 'contain') {
|
||||
$activities = $activities->where('prehead_time', '>=', $range[0])->where('end_time', '<=', $range[1]);
|
||||
}
|
||||
|
||||
$activities = $activities->select();
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取商品的所有正在进行,或正在预售的活动
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsActivitys($goods_id)
|
||||
{
|
||||
$classify = $this->model->classifies();
|
||||
$activityTypes = array_keys($classify['activity']);
|
||||
|
||||
$activities = $this->model->show()->where('find_in_set(:id,goods_ids)', ['id' => $goods_id])
|
||||
->with(['activity_sku_prices' => function ($query) use ($goods_id) {
|
||||
$query->where('goods_id', $goods_id)
|
||||
->where(
|
||||
'status',
|
||||
'up'
|
||||
);
|
||||
}])
|
||||
->where('type', 'in', $activityTypes)
|
||||
->order('start_time', 'asc') // 优先查询最先开始的活动(允许商品同时存在多个活动中, 只要开始结束时间不重合)
|
||||
->select();
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取商品的所有正在进行,或正在预售的营销
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsPromos($goods_id)
|
||||
{
|
||||
$classify = $this->model->classifies();
|
||||
$activityTypes = array_keys($classify['promo']);
|
||||
|
||||
$promos = $this->model->show()
|
||||
->where(function ($query) use ($goods_id) {
|
||||
// goods_ids 里面有当前商品,或者 goods_ids 为null(所有商品都参与)
|
||||
$query->where('find_in_set('. $goods_id .',goods_ids)')
|
||||
->whereOr('goods_ids', null)
|
||||
->whereOr('goods_ids', '');
|
||||
})
|
||||
->where('type', 'in', $activityTypes)
|
||||
->order('start_time', 'asc') // 优先查询最先开始的活动(允许商品同时存在多个活动中, 只要开始结束时间不重合)
|
||||
->select();
|
||||
|
||||
return $promos;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 活动 id 获取指定活动
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @param integer $activity_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsActivityByActivity($goods_id, $activity_id)
|
||||
{
|
||||
$classify = $this->model->classifies();
|
||||
$activityTypes = array_keys($classify['activity']);
|
||||
|
||||
$activity = $this->model->where('id', $activity_id)->find();
|
||||
|
||||
if ($activity) {
|
||||
$goods_ids = array_values(array_filter(explode(',', $activity->goods_ids)));
|
||||
if (!in_array($goods_id, $goods_ids) && !empty($goods_ids)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($activity['type'], $activityTypes)) {
|
||||
// 活动规格
|
||||
$activity->activity_sku_prices = $activity->activity_sku_prices;
|
||||
}
|
||||
|
||||
$activity = $activity->toArray();
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
||||
124
addons/shopro/library/activity/getter/Redis.php
Normal file
124
addons/shopro/library/activity/getter/Redis.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\getter;
|
||||
|
||||
class Redis extends Base
|
||||
{
|
||||
public $redis;
|
||||
|
||||
/**
|
||||
* 获取所有给定类型给定状态的活动
|
||||
*
|
||||
* @param array $activityTypes
|
||||
* @return array
|
||||
*/
|
||||
public function getActivities($activityTypes, $status = 'all')
|
||||
{
|
||||
$activities = $this->redis->getActivityList($activityTypes, $status, 'clear');
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定时间区间内的活动
|
||||
*
|
||||
* @param array $range
|
||||
* @param array $activityTypes
|
||||
* @param string $range_type overlap=只要区间有重叠的就算|contain=包含,必须在这个区间之内的
|
||||
* @return array
|
||||
*/
|
||||
public function getActivitiesByRange($range, $activityTypes = [], $range_type = 'overlap')
|
||||
{
|
||||
$activities = $this->redis->getActivityList($activityTypes);
|
||||
|
||||
$newActivities = [];
|
||||
foreach ($activities as $key => $activity) {
|
||||
if ($this->rangeCompare($range, [$activity['prehead_time'], $activity['end_time']], $range_type)) {
|
||||
$newActivities[] = $activity;
|
||||
}
|
||||
}
|
||||
|
||||
return $newActivities;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取商品的所有正在进行,或正在预售的活动
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsActivitys($goods_id)
|
||||
{
|
||||
$classify = $this->model->classifies();
|
||||
$activityTypes = array_keys($classify['activity']);
|
||||
|
||||
$activities = $this->redis->getGoodsActivitys($goods_id, $activityTypes, ['prehead', 'ing']);
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取商品的所有正在进行,或正在预售的营销
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsPromos($goods_id)
|
||||
{
|
||||
$classify = $this->model->classifies();
|
||||
$activityTypes = array_keys($classify['promo']);
|
||||
|
||||
$activities = $this->redis->getGoodsActivitys($goods_id, $activityTypes, ['prehead', 'ing'], 'promo');
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 活动 id 获取指定活动
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @param integer $activity_id
|
||||
* @return array
|
||||
*/
|
||||
public function getGoodsActivityByActivity($goods_id, $activity_id)
|
||||
{
|
||||
$activities = $this->redis->getGoodsActivityByActivity($goods_id, $activity_id);
|
||||
|
||||
return $activities;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 比较时间区间
|
||||
*
|
||||
* @param array $range
|
||||
* @param array $activityRange
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
private function rangeCompare($range, $activityRange, $type = 'overlap')
|
||||
{
|
||||
if ($type == 'overlap') {
|
||||
if ($range[1] >= $activityRange[0] && $range[0] <= $activityRange[1]) { // 时间相等也算没有交集
|
||||
// 两个时间区间有交集
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} elseif ($type == 'contain') {
|
||||
if ($range[0] <= $activityRange[0] && $range[1] >= $activityRange[1]) { // 时间相等算包含
|
||||
// activityRange 是 range 的子集
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
235
addons/shopro/library/activity/provider/Base.php
Normal file
235
addons/shopro/library/activity/provider/Base.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
use addons\shopro\library\activity\Activity as ActivityManager;
|
||||
use addons\shopro\library\activity\contract\ActivityInterface;
|
||||
use addons\shopro\library\activity\traits\CheckActivity;
|
||||
use app\admin\model\shopro\activity\SkuPrice as ActivitySkuPriceModel;
|
||||
|
||||
abstract class Base implements ActivityInterface
|
||||
{
|
||||
use CheckActivity;
|
||||
|
||||
/**
|
||||
* ActivityManager
|
||||
*
|
||||
* @var ActivityManager
|
||||
*/
|
||||
protected $manager = null;
|
||||
|
||||
protected $rules = [];
|
||||
|
||||
|
||||
protected $message = [];
|
||||
|
||||
|
||||
protected $default = [];
|
||||
|
||||
|
||||
public function __construct(ActivityManager $activityManager)
|
||||
{
|
||||
$this->manager = $activityManager;
|
||||
}
|
||||
|
||||
|
||||
public function validate($data)
|
||||
{
|
||||
$data = array_merge($this->default, $data);
|
||||
|
||||
$validate = (new \think\Validate)->message($this->message)->rule($this->rules);
|
||||
if (!$validate->check($data)) {
|
||||
error_stop($validate->getError());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
public function check($params)
|
||||
{
|
||||
if ((isset($params['start_time']) && $params['start_time'] > $params['end_time']) || $params['end_time'] < date('Y-m-d H:i:s')) {
|
||||
error_stop('请设置正确的活动时间');
|
||||
}
|
||||
if (isset($params['prehead_time']) && $params['prehead_time'] > $params['start_time']) {
|
||||
error_stop('预热时间必须小于活动开始时间');
|
||||
}
|
||||
|
||||
$rules = $this->validate($params['rules']);
|
||||
|
||||
// 存在折扣,将折扣按照从小到大排序
|
||||
if (isset($rules['discounts']) && $rules['discounts']) {
|
||||
// 处理展示优惠,full 从小到大
|
||||
$discounts = $rules['discounts'];
|
||||
|
||||
$discountsKeys = array_column($discounts, null, 'full');
|
||||
ksort($discountsKeys);
|
||||
$rules['discounts'] = array_values($discountsKeys); // 优惠按照 full 从小到大排序
|
||||
}
|
||||
|
||||
$params['rules'] = $rules;
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 附加活动信息
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $rules
|
||||
* @return array
|
||||
*/
|
||||
public function rulesInfo($type, $rules)
|
||||
{
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function save($activity, $params = [])
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function showSkuPrice($skuPrice)
|
||||
{
|
||||
return $skuPrice;
|
||||
}
|
||||
|
||||
|
||||
public function formatTags($rules, $type)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function formatTag($discountData)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function formatTexts($rules, $type)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function recoverSkuPrices($goods, $activity)
|
||||
{
|
||||
return $goods['sku_prices'];
|
||||
}
|
||||
|
||||
|
||||
public function buyCheck($buyInfo, $activity)
|
||||
{
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
public function buy($buyInfo, $activity)
|
||||
{
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
public function buyOk($order, $user)
|
||||
{
|
||||
return $order;
|
||||
}
|
||||
|
||||
|
||||
public function buyFail($order, $type)
|
||||
{
|
||||
return $order;
|
||||
}
|
||||
|
||||
|
||||
public function getPromoInfo($promo, ?array $data = [])
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
protected function promoGoodsData($promo)
|
||||
{
|
||||
$promo_goods_amount = '0'; // 该活动中商品的总价
|
||||
$promo_goods_num = '0'; // 该活动商品总件数
|
||||
$goodsIds = []; // 该活动中所有的商品 id
|
||||
$promo_dispatch_amount = '0'; // 该活动中总运费
|
||||
|
||||
// 活动中的商品总金额,总件数,所有商品 id
|
||||
foreach ($promo['goods'] as $buyInfo) {
|
||||
$promo_goods_amount = bcadd($promo_goods_amount, (string)$buyInfo['goods_amount'], 2);
|
||||
$promo_goods_num = bcadd($promo_goods_num, (string)$buyInfo['goods_num'], 2);
|
||||
$goodsIds[] = $buyInfo['goods_id'];
|
||||
|
||||
$promo_dispatch_amount = bcadd($promo_dispatch_amount, (string)$buyInfo['dispatch_amount'], 2);
|
||||
}
|
||||
|
||||
return compact(
|
||||
"promo_goods_amount",
|
||||
"promo_goods_num",
|
||||
"promo_dispatch_amount",
|
||||
"goodsIds"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加,编辑活动规格,type = stock 只编辑库存
|
||||
*
|
||||
* @param array $goodsList 商品列表
|
||||
* @param int $activity_id 活动 id
|
||||
* @param string $type type = all 全部编辑,type = stock 只编辑库存
|
||||
* @return void
|
||||
*/
|
||||
protected function saveSkuPrice($goodsList, $activity, \Closure $extCb = null)
|
||||
{
|
||||
//如果是编辑 先下架所有的规格产品,防止丢失历史销量数据;
|
||||
|
||||
$type = 'all';
|
||||
if (request()->isPut() && $activity->status == 'ing') {
|
||||
// 修改并且是进行中的活动,只能改库存
|
||||
$type = 'stock';
|
||||
}
|
||||
|
||||
ActivitySkuPriceModel::where('activity_id', $activity->id)->update(['status' => 'down']);
|
||||
|
||||
foreach ($goodsList as $key => $goods) {
|
||||
$actSkuPrice[$key] = $goods['activity_sku_prices'];
|
||||
|
||||
foreach ($actSkuPrice[$key] as $ke => $skuPrice) {
|
||||
if ($type == 'all') {
|
||||
$current = $skuPrice;
|
||||
// 处理 ext 回调
|
||||
if ($extCb) {
|
||||
$current = $extCb($current);
|
||||
}
|
||||
} else {
|
||||
$current = [
|
||||
'id' => $skuPrice['id'],
|
||||
'stock' => $skuPrice['stock'],
|
||||
'status' => $skuPrice['status'] // 这个要去掉,不能改参与状态
|
||||
];
|
||||
}
|
||||
|
||||
if ($current['id'] == 0) {
|
||||
unset($current['id']);
|
||||
}
|
||||
unset($current['sales']);
|
||||
$current['activity_id'] = $activity->id;
|
||||
$current['goods_id'] = $goods['id'];
|
||||
|
||||
$actSkuPriceModel = new ActivitySkuPriceModel();
|
||||
if (isset($current['id'])) {
|
||||
// type == 'edit'
|
||||
$actSkuPriceModel = $actSkuPriceModel->find($current['id']);
|
||||
}
|
||||
|
||||
if ($actSkuPriceModel) {
|
||||
unset($current['createtime'], $current['updatetime']);
|
||||
$actSkuPriceModel->allowField(true)->save($current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
148
addons/shopro/library/activity/provider/FreeShipping.php
Normal file
148
addons/shopro/library/activity/provider/FreeShipping.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
/**
|
||||
* 满额包邮
|
||||
*/
|
||||
class FreeShipping extends Base
|
||||
{
|
||||
protected $rules = [
|
||||
"type" => "require",
|
||||
"full_num" => "require|float"
|
||||
];
|
||||
|
||||
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"type" => "money", // money=满足金额|num=满足件数
|
||||
"province_except" => '', // 不包邮的省份
|
||||
"city_except" => '', // 不包邮的城市
|
||||
"district_except" => '', // 不包邮的地区
|
||||
"district_text" => [], // 中文
|
||||
"full_num" => 0
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
public function formatTags($rules, $type)
|
||||
{
|
||||
$full_num = $rules['full_num'] ?? ($rules['discounts'][0]['full_num'] ?? 0); // activity_order 存的格式不一样是在 discount 里面包裹着
|
||||
$tags[] = $this->formatTag([
|
||||
'type' => $rules['type'],
|
||||
'full_num' => $full_num,
|
||||
]);
|
||||
|
||||
return array_values(array_filter($tags));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化 discount 折扣为具体优惠标签
|
||||
*
|
||||
* @param array $discountData
|
||||
* @return string
|
||||
*/
|
||||
public function formatTag($discountData)
|
||||
{
|
||||
$tag = '满' . $discountData['full_num'] . ($discountData['type'] == 'money' ? '元' : '件') . '包邮';
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化 discount 折扣为具体优惠详情
|
||||
*/
|
||||
public function formatTexts($rules, $type)
|
||||
{
|
||||
$text = '满' . $rules['full_num'] . ($rules['type'] == 'money' ? '元' : '件') . '即可包邮';
|
||||
if (isset($rules['district_text']) && $rules['district_text']) {
|
||||
$district = '';
|
||||
if (isset($rules['district_text']['province']) && $rules['district_text']['province']) {
|
||||
$district .= join(',', $rules['district_text']['province']) . ',';
|
||||
}
|
||||
if (isset($rules['district_text']['city']) && $rules['district_text']['city']) {
|
||||
$district .= join(',', $rules['district_text']['city']) . ',';
|
||||
}
|
||||
if (isset($rules['district_text']['district']) && $rules['district_text']['district']) {
|
||||
$district .= join(',', $rules['district_text']['district']) . ',';
|
||||
}
|
||||
|
||||
if ($district) {
|
||||
$text .= " (不支持包邮地区:" . rtrim($district, ',') . ")";
|
||||
}
|
||||
}
|
||||
|
||||
$texts[] = $text;
|
||||
|
||||
return array_values(array_filter($texts));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function getPromoInfo($promo, $data = [])
|
||||
{
|
||||
extract($this->promoGoodsData($promo));
|
||||
$rules = $promo['rules'];
|
||||
$userAddress = $data['userAddress'] ?? null;
|
||||
|
||||
// 是按金额,还是按件数比较
|
||||
$compareif = $rules['type'] == 'num' ? 'promo_goods_num' : 'promo_goods_amount';
|
||||
|
||||
// 判断除外的地区
|
||||
$district_except = isset($rules['district_except']) && $rules['district_except'] ? explode(',', $rules['district_except']) : [];
|
||||
$city_except = isset($rules['city_except']) && $rules['city_except'] ? explode(',', $rules['city_except']) : [];
|
||||
$province_except = isset($rules['province_except']) && $rules['province_except'] ? explode(',', $rules['province_except']) : [];
|
||||
if ($userAddress) {
|
||||
if (
|
||||
in_array($userAddress['district_id'], $district_except)
|
||||
|| in_array($userAddress['city_id'], $city_except)
|
||||
|| in_array($userAddress['province_id'], $province_except)
|
||||
) {
|
||||
// 收货地址在非包邮地区,则继续循环下个活动
|
||||
return null;
|
||||
}
|
||||
} else if ($district_except || $city_except || $province_except) {
|
||||
// 没有选择收货地址,并且活动中包含地区限制,不计算活动
|
||||
return null;
|
||||
}
|
||||
|
||||
if (${$compareif} < $rules['full_num']) {
|
||||
// 不满足条件,接着循环下个规则
|
||||
return null;
|
||||
}
|
||||
|
||||
// 记录活动信息
|
||||
$promo_discount_info = [
|
||||
'activity_id' => $promo['id'], // 活动id
|
||||
'activity_title' => $promo['title'], // 活动标题
|
||||
'activity_type' => $promo['type'], // 活动类型
|
||||
'activity_type_text' => $promo['type_text'], // 活动类型中文
|
||||
'promo_discount_money' => 0, // 这里无法知道真实运费减免,会在 orderCreate 后续计算完包邮优惠之后,改为真实减免的运费
|
||||
'promo_goods_amount' => $promo_goods_amount, // 当前活动商品总金额
|
||||
'rule_type' => $rules['type'], // 满多少元|还是满多少件
|
||||
'discount_rule' => [
|
||||
'full_num' => $rules['full_num']
|
||||
], // 满足的那条规则
|
||||
'goods_ids' => $goodsIds // 这个活动包含的这次购买的商品
|
||||
];
|
||||
|
||||
return $promo_discount_info;
|
||||
}
|
||||
}
|
||||
132
addons/shopro/library/activity/provider/FullDiscount.php
Normal file
132
addons/shopro/library/activity/provider/FullDiscount.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
/**
|
||||
* 满额折扣
|
||||
*/
|
||||
class FullDiscount extends Base
|
||||
{
|
||||
protected $rules = [
|
||||
"type" => "require",
|
||||
"discounts" => "require|array"
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
"discounts.require" => '请填写优惠规则',
|
||||
"discounts.array" => '请填写优惠规则',
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"type" => "money", // money=满足金额|num=满足件数
|
||||
"discounts" => []
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
public function formatTags($rules, $type)
|
||||
{
|
||||
$tags = [];
|
||||
$discounts = $rules['discounts'] ?? [];
|
||||
|
||||
foreach ($discounts as $discount) {
|
||||
$tags[] = $this->formatTag([
|
||||
'type' => $rules['type'],
|
||||
'full' => $discount['full'],
|
||||
'discount' => $discount['discount']
|
||||
]);
|
||||
}
|
||||
|
||||
return array_values(array_filter($tags));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化 discount 折扣为具体优惠标签
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $discountData
|
||||
* @return string
|
||||
*/
|
||||
public function formatTag($discountData)
|
||||
{
|
||||
$tag = '满' . $discountData['full'] . ($discountData['type'] == 'money' ? '元' : '件');
|
||||
$tag .= $discountData['discount'] . '折';
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function formatTexts($rules, $type)
|
||||
{
|
||||
$texts = [];
|
||||
$discounts = $rules['discounts'] ?? [];
|
||||
|
||||
foreach ($discounts as $discount) {
|
||||
$text = '满' . $discount['full'] . ($rules['type'] == 'money' ? '元' : '件');
|
||||
$text .= ',商品总价打' . $discount['discount'] . '折';
|
||||
|
||||
$texts[] = $text;
|
||||
}
|
||||
|
||||
return array_values(array_filter($texts));
|
||||
}
|
||||
|
||||
|
||||
public function getPromoInfo($promo, $data = [])
|
||||
{
|
||||
extract($this->promoGoodsData($promo));
|
||||
$rules = $promo['rules'];
|
||||
|
||||
// 是按金额,还是按件数比较
|
||||
$compareif = $rules['type'] == 'num' ? 'promo_goods_num' : 'promo_goods_amount';
|
||||
|
||||
// 将规则按照从大到校排列,优先比较是否满足最大规则
|
||||
$rulesDiscounts = isset($rules['discounts']) && $rules['discounts'] ? array_reverse($rules['discounts']) : []; // 数组反转
|
||||
|
||||
// 满减, 满折多个规则从大到小匹配最优惠
|
||||
foreach ($rulesDiscounts as $d) {
|
||||
if (${$compareif} < $d['full']) {
|
||||
// 不满足条件,接着循环下个规则
|
||||
continue;
|
||||
}
|
||||
|
||||
$dis = bcdiv($d['discount'], '10', 3); // 保留三位小数,转化折扣
|
||||
$dis = $dis > 1 ? 1 : ($dis < 0 ? 0 : $dis); // 定义边界 0 - 1
|
||||
$promo_dis = 1 - $dis;
|
||||
|
||||
$current_promo_discount_money = bcmul($promo_goods_amount, (string)$promo_dis, 3);
|
||||
$current_promo_discount_money = number_format((float)$current_promo_discount_money, 2, '.', ''); // 计算折扣金额,四舍五入
|
||||
|
||||
// 记录该活动的一些统计信息
|
||||
$promo_discount_info = [
|
||||
'activity_id' => $promo['id'], // 活动id
|
||||
'activity_title' => $promo['title'], // 活动标题
|
||||
'activity_type' => $promo['type'], // 活动类型
|
||||
'activity_type_text' => $promo['type_text'], // 活动类型中文
|
||||
'promo_discount_money' => $current_promo_discount_money, // 优惠金额
|
||||
'promo_goods_amount' => $promo_goods_amount, // 当前活动商品总金额
|
||||
'rule_type' => $rules['type'], // 满多少元|还是满多少件
|
||||
'discount_rule' => $d, // 满足的那条规则
|
||||
'goods_ids' => $goodsIds // 这个活动包含的这次购买的商品
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return $promo_discount_info ?? null;
|
||||
}
|
||||
}
|
||||
248
addons/shopro/library/activity/provider/FullGift.php
Normal file
248
addons/shopro/library/activity/provider/FullGift.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
use addons\shopro\library\activity\traits\GiveGift;
|
||||
use app\admin\model\shopro\Coupon;
|
||||
use app\admin\model\shopro\order\Order;
|
||||
|
||||
/**
|
||||
* 满额赠送
|
||||
*/
|
||||
class FullGift extends Base
|
||||
{
|
||||
use GiveGift;
|
||||
|
||||
protected $rules = [
|
||||
"limit_num" => "number|egt:0",
|
||||
"type" => "require",
|
||||
"event" => "require",
|
||||
"discounts" => "require|array"
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
"discounts.require" => '请填写优惠规则',
|
||||
"discounts.array" => '请填写优惠规则',
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"limit_num" => 0, // 每人可参与次数 0:不限制
|
||||
"type" => "money", // money=满足金额|num=满足件数
|
||||
"event" => "confirm", // 赠送时机 paid=支付完成|confirm=确认收货(必须全部确认收货才可以)|finish=订单完成(评价完成)
|
||||
"discounts" => [] //{"full":"100",
|
||||
// "types":"coupon=优惠券|score=积分|money=余额|goods=商品",
|
||||
// "coupon_ids":"赠优惠券时存在",
|
||||
// "total":"赠送优惠券总金额",
|
||||
// "score":"积分",
|
||||
// "money":"余额",
|
||||
// "goods_ids":"商品时存在",
|
||||
// "gift_num":"礼品份数"}
|
||||
];
|
||||
|
||||
protected $giftType = [
|
||||
'money' => '余额',
|
||||
'score' => '积分',
|
||||
'coupon' => '优惠券',
|
||||
'goods' => '商品',
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 验证赠送规则字段
|
||||
$this->checkGiftDiscount($params['rules']['discounts']);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取 赠送的优惠券列表
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $rules
|
||||
* @return array
|
||||
*/
|
||||
public function rulesInfo($type, $rules)
|
||||
{
|
||||
$discounts = $rules['discounts'];
|
||||
foreach ($discounts as &$discount) {
|
||||
$discount['coupon_list'] = [];
|
||||
if (in_array('coupon', $discount['types']) && isset($discount['coupon_ids']) && $discount['coupon_ids']) {
|
||||
$discount['coupon_list'] = Coupon::statusHidden()->whereIn('id', $discount['coupon_ids'])->select();
|
||||
}
|
||||
}
|
||||
$rules['discounts'] = $discounts;
|
||||
return $rules;
|
||||
}
|
||||
|
||||
|
||||
public function formatTags($rules, $type)
|
||||
{
|
||||
$tags = [];
|
||||
$discounts = $rules['discounts'] ?? [];
|
||||
|
||||
foreach ($discounts as $discount) {
|
||||
$tags[] = $this->formatTag([
|
||||
'type' => $rules['type'],
|
||||
'simple' => $rules['simple'] ?? false, // 简单信息展示
|
||||
'full' => $discount['full'],
|
||||
'discount' => $discount
|
||||
]);
|
||||
}
|
||||
|
||||
return array_values(array_filter($tags));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化 discount 折扣为具体优惠标签
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $discountData
|
||||
* @return string
|
||||
*/
|
||||
public function formatTag($discountData)
|
||||
{
|
||||
$discount = $discountData['discount'];
|
||||
$gift_type_text = '';
|
||||
foreach ($discount['types'] as $type) {
|
||||
$gift_type_text = $gift_type_text . (isset($this->giftType[$type]) ? ',' . $this->giftType[$type] : '');
|
||||
}
|
||||
|
||||
$tag = '满' . $discountData['full'] . ($discountData['type'] == 'money' ? '元' : '件');
|
||||
$tag .= '赠送' . ($discountData['simple'] ? '礼品' : trim($gift_type_text, ','));
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
|
||||
public function formatTexts($rules, $type)
|
||||
{
|
||||
$texts = [];
|
||||
$discounts = $rules['discounts'] ?? [];
|
||||
|
||||
foreach ($discounts as $discount) {
|
||||
$text = '消费满' . $discount['full'] . ($rules['type'] == 'money' ? '元' : '件');
|
||||
$text .= '';
|
||||
foreach ($discount['types'] as $type) {
|
||||
$text .= ',';
|
||||
if ($type == 'money') {
|
||||
$text .= '赠送' . $discount['money'] . '元余额 ';
|
||||
} elseif ($type == 'score') {
|
||||
$text .= '赠送' . $discount['score'] . '积分 ';
|
||||
} elseif ($type == 'coupon') {
|
||||
$text .= '赠送价值' . $discount['total'] . '元优惠券 ';
|
||||
}
|
||||
}
|
||||
|
||||
$text .= ' (条件:活动礼品共 ' . $discount['gift_num'] . ' 份';
|
||||
if ($rules['limit_num'] > 0) {
|
||||
$text .= ',每人仅限参与 ' . $rules['limit_num'] . ' 次';
|
||||
}
|
||||
$text .= ')';
|
||||
$texts[] = $text;
|
||||
}
|
||||
|
||||
return array_values(array_filter($texts));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getPromoInfo($promo, $data = [])
|
||||
{
|
||||
extract($this->promoGoodsData($promo));
|
||||
$rules = $promo['rules'];
|
||||
|
||||
// 是按金额,还是按件数比较
|
||||
$compareif = $rules['type'] == 'num' ? 'promo_goods_num' : 'promo_goods_amount';
|
||||
|
||||
// 将规则按照从大到校排列,优先比较是否满足最大规则
|
||||
$rulesDiscounts = isset($rules['discounts']) && $rules['discounts'] ? array_reverse($rules['discounts']) : []; // 数组反转
|
||||
|
||||
// 满减, 满折多个规则从大到小匹配最优惠
|
||||
foreach ($rulesDiscounts as $d) {
|
||||
unset($d['coupon_list']); // 移除规则里面的 coupon_list
|
||||
if (${$compareif} < $d['full']) {
|
||||
// 不满足条件,接着循环下个规则
|
||||
continue;
|
||||
}
|
||||
|
||||
// 记录该活动的一些统计信息
|
||||
$promo_discount_info = [
|
||||
'activity_id' => $promo['id'], // 活动id
|
||||
'activity_title' => $promo['title'], // 活动标题
|
||||
'activity_type' => $promo['type'], // 活动类型
|
||||
'activity_type_text' => $promo['type_text'], // 活动类型中文
|
||||
'promo_discount_money' => 0, // 优惠金额 (赠送,不优惠)
|
||||
'promo_goods_amount' => $promo_goods_amount, // 当前活动商品总金额
|
||||
'rule_type' => $rules['type'], // 满多少元|还是满多少件
|
||||
'discount_rule' => $d, // 满足的那条规则
|
||||
"limit_num" => $rules['limit_num'], // 每个人可参与次数
|
||||
"event" => $rules['event'], // 赠送时机
|
||||
'goods_ids' => $goodsIds // 这个活动包含的这次购买的商品
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return $promo_discount_info ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 支付成功(货到付款下单),添加礼品记录
|
||||
*
|
||||
* @param array|object $order
|
||||
* @param array|object $user
|
||||
* @return void
|
||||
*/
|
||||
public function buyOk($order, $user)
|
||||
{
|
||||
// 满赠送
|
||||
$ext = $order->ext;
|
||||
$promoInfos = $ext['promo_infos'];
|
||||
|
||||
foreach ($promoInfos as $info) {
|
||||
if ($info['activity_type'] == 'full_gift') {
|
||||
// 满赠,开始赠送
|
||||
$this->addGiftsLog($order, $user, $info);
|
||||
}
|
||||
}
|
||||
|
||||
$event = $order->status == Order::STATUS_PENDING ? 'pending' : 'paid'; // 货到付款不是真的付款,不能发放礼品 event 改为 pending
|
||||
|
||||
// 检测并赠送礼品
|
||||
$this->checkAndGift($order, $user, $promoInfos, $event);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 促销购买失败(退款)
|
||||
*
|
||||
* @param \think\Model $order
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function buyFail($order, $type)
|
||||
{
|
||||
if ($type == 'refund') {
|
||||
// 退款,将礼品标记为已退款,如果已经送出去的不扣除
|
||||
$this->checkAndFailGift($order, '订单全额退款');
|
||||
} else if ($type == 'invalid') {
|
||||
if ($order->pay_mode == 'offline') {
|
||||
// 只有线下付款取消时才需要标记礼品赠送失败
|
||||
$this->checkAndFailGift($order, '货到付款订单取消');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
addons/shopro/library/activity/provider/FullReduce.php
Normal file
128
addons/shopro/library/activity/provider/FullReduce.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
/**
|
||||
* 满额立减
|
||||
*/
|
||||
class FullReduce extends Base
|
||||
{
|
||||
protected $rules = [
|
||||
"type" => "require",
|
||||
"discounts" => "require|array"
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
"discounts.require" => '请填写优惠规则',
|
||||
"discounts.array" => '请填写优惠规则',
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"type" => "money", // money=满足金额|num=满足件数
|
||||
"discounts" => []
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
public function formatTags($rules, $type)
|
||||
{
|
||||
$tags = [];
|
||||
$discounts = $rules['discounts'] ?? [];
|
||||
|
||||
foreach ($discounts as $discount) {
|
||||
$tags[] = self::formatTag([
|
||||
'type' => $rules['type'],
|
||||
'full' => $discount['full'],
|
||||
'discount' => $discount['discount']
|
||||
]);
|
||||
}
|
||||
|
||||
return array_values(array_filter($tags));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 discount 折扣为具体优惠标签
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $discountData
|
||||
* @return string
|
||||
*/
|
||||
public function formatTag($discountData)
|
||||
{
|
||||
$tag = '满' . $discountData['full'] . ($discountData['type'] == 'money' ? '元' : '件');
|
||||
$tag .= '减' . $discountData['discount'];
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function formatTexts($rules, $type)
|
||||
{
|
||||
$texts = [];
|
||||
$discounts = $rules['discounts'] ?? [];
|
||||
|
||||
foreach ($discounts as $discount) {
|
||||
$text = '满' . $discount['full'] . ($rules['type'] == 'money' ? '元' : '件');
|
||||
$text .= '减' . $discount['discount'] . '元';
|
||||
|
||||
$texts[] = $text;
|
||||
}
|
||||
|
||||
return array_values(array_filter($texts));
|
||||
}
|
||||
|
||||
|
||||
public function getPromoInfo($promo, $data = [])
|
||||
{
|
||||
extract($this->promoGoodsData($promo));
|
||||
$rules = $promo['rules'];
|
||||
|
||||
// 是按金额,还是按件数比较
|
||||
$compareif = $rules['type'] == 'num' ? 'promo_goods_num' : 'promo_goods_amount';
|
||||
|
||||
// 将规则按照从大到校排列,优先比较是否满足最大规则
|
||||
$rulesDiscounts = isset($rules['discounts']) && $rules['discounts'] ? array_reverse($rules['discounts']) : []; // 数组反转
|
||||
|
||||
// 满减, 满折多个规则从大到小匹配最优惠
|
||||
foreach ($rulesDiscounts as $d) {
|
||||
if (${$compareif} < $d['full']) {
|
||||
// 不满足条件,接着循环下个规则
|
||||
continue;
|
||||
}
|
||||
|
||||
// 满足优惠
|
||||
$current_promo_discount_money = (isset($d['discount']) && $d['discount']) ? $d['discount'] : 0;
|
||||
$current_promo_discount_money = number_format((float)$current_promo_discount_money, 2, '.', ''); // 格式化金额,四舍五入
|
||||
|
||||
// 记录该活动的一些统计信息
|
||||
$promo_discount_info = [
|
||||
'activity_id' => $promo['id'], // 活动id
|
||||
'activity_title' => $promo['title'], // 活动标题
|
||||
'activity_type' => $promo['type'], // 活动类型
|
||||
'activity_type_text' => $promo['type_text'], // 活动类型中文
|
||||
'promo_discount_money' => $current_promo_discount_money, // 优惠金额
|
||||
'promo_goods_amount' => $promo_goods_amount, // 当前活动商品总金额
|
||||
'rule_type' => $rules['type'], // 满多少元|还是满多少件
|
||||
'discount_rule' => $d, // 满足的那条规则
|
||||
'goods_ids' => $goodsIds // 这个活动包含的这次购买的商品
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return $promo_discount_info ?? null;
|
||||
}
|
||||
}
|
||||
275
addons/shopro/library/activity/provider/Groupon.php
Normal file
275
addons/shopro/library/activity/provider/Groupon.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
use addons\shopro\library\activity\traits\Groupon as GrouponTrait;
|
||||
use addons\shopro\service\StockSale;
|
||||
use addons\shopro\exception\ShoproException;
|
||||
|
||||
/**
|
||||
* 普通拼团
|
||||
*/
|
||||
class Groupon extends Base
|
||||
{
|
||||
use GrouponTrait;
|
||||
|
||||
protected $rules = [
|
||||
"is_commission" => "require|boolean",
|
||||
"is_free_shipping" => "require|boolean",
|
||||
"sales_show_type" => "require",
|
||||
"team_num" => "require|number",
|
||||
"is_alone" => "require|boolean",
|
||||
"is_fictitious" => "require|boolean",
|
||||
"fictitious_time" => "requireIf:is_fictitious,1|float|egt:0",
|
||||
"is_team_card" => "require|boolean",
|
||||
"is_leader_discount" => "require|boolean",
|
||||
"valid_time" => "require|float|egt:0",
|
||||
"limit_num" => "number|egt:0",
|
||||
"refund_type" => "require", // 退款方式 back=原路退回|money=退回到余额
|
||||
"order_auto_close" => "float|egt:0",
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
'team_num.require' => '请填写成团人数',
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"is_commission" => 0, // 是否参与分销
|
||||
"is_free_shipping" => 0, // 是否包邮
|
||||
"sales_show_type" => "real", // real=真实活动销量|goods=商品总销量(包含虚拟销量)
|
||||
"team_num" => 2, // 成团人数,最少两人
|
||||
"is_alone" => 0, // 是否允许单独购买
|
||||
"is_fictitious" => 0, // 是否允许虚拟成团
|
||||
"fictitious_num" => 0, // 最多虚拟人数 0:不允许虚拟 '' 不限制
|
||||
"fictitious_time" => 0, // 开团多长时间自动虚拟成团
|
||||
"is_team_card" => 0, // 参团卡显示
|
||||
"is_leader_discount" => 0, // 团长优惠
|
||||
"valid_time" => 0, // 组团有效时间, 0:一直有效
|
||||
"limit_num" => 0, // 每人限购数量 0:不限购
|
||||
"refund_type" => "back", // 退款方式 back=原路退回|money=退回到余额
|
||||
"order_auto_close" => 0, // 订单自动关闭时间,如果为 0 将使用系统级订单自动关闭时间
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 验证添加的活动商品是否至少设置了一个活动规格
|
||||
$this->checkActivitySkuPrice($params['goods_list']);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
public function save($activity, $params = [])
|
||||
{
|
||||
$goodsList = $params['goods_list'];
|
||||
|
||||
$this->saveSkuPrice($goodsList, $activity, function ($skuPrice) use ($activity) {
|
||||
// 处理 团长优惠
|
||||
$rules = $activity->rules;
|
||||
$is_leader_discount = $rules['is_leader_discount'] ?? 0;
|
||||
$leader_price = $skuPrice['price'];
|
||||
if ($is_leader_discount && isset($skuPrice['leader_price'])) {
|
||||
$leader_price = $skuPrice['leader_price'];
|
||||
}
|
||||
$ext = [
|
||||
'is_leader_discount' => $is_leader_discount,
|
||||
'leader_price' => number_format(floatval($leader_price), 2, '.', '')
|
||||
];
|
||||
unset($skuPrice['leader_price']);
|
||||
$skuPrice['ext'] = $ext;
|
||||
|
||||
return $skuPrice;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function showSkuPrice($skuPrice)
|
||||
{
|
||||
$ext = $skuPrice['ext'] ?? [];
|
||||
|
||||
$skuPrice['leader_price'] = $ext['leader_price'] ?? $skuPrice['price'];
|
||||
|
||||
return $skuPrice;
|
||||
}
|
||||
|
||||
|
||||
public function recoverSkuPrices($goods, $activity)
|
||||
{
|
||||
$activitySkuPrices = $activity['activity_sku_prices'];
|
||||
$skuPrices = $goods->sku_prices;
|
||||
|
||||
foreach ($skuPrices as $key => &$skuPrice) {
|
||||
$stock = $skuPrice->stock; // 下面要用
|
||||
$skuPrice->stock = 0;
|
||||
$skuPrice->sales = 0;
|
||||
foreach ($activitySkuPrices as $activitySkuPrice) {
|
||||
if ($skuPrice->id == $activitySkuPrice['goods_sku_price_id']) {
|
||||
// 采用活动的 规格内容
|
||||
$is_leader_discount = $activitySkuPrice['ext']['is_leader_discount'];
|
||||
$leader_price = $activitySkuPrice['ext']['leader_price'];
|
||||
$skuPrice->old_price = $skuPrice->price; // 保存原始普通商品规格的价格(计算活动的优惠)
|
||||
$skuPrice->stock = ($activitySkuPrice['stock'] > $stock) ? $stock : $activitySkuPrice['stock']; // 活动库存不能超过商品库存
|
||||
$skuPrice->sales = $activitySkuPrice['sales'];
|
||||
$skuPrice->groupon_price = $activitySkuPrice['price']; // 不覆盖原来规格价格,用作单独购买,将活动的价格设置为新的拼团价格
|
||||
$skuPrice->is_leader_discount = $is_leader_discount; // 是否团长优惠
|
||||
$skuPrice->leader_price = $leader_price; // 团长优惠价格
|
||||
$skuPrice->status = $activitySkuPrice['status']; // 采用活动的上下架
|
||||
$skuPrice->ext = $activitySkuPrice['ext']; // 活动规格 ext, order_item 保存备用
|
||||
$skuPrice->min_price = $activitySkuPrice['price']; // 当前活动规格最小价格,这里是拼团价
|
||||
$skuPrice->max_price = $activitySkuPrice['price']; // 用作计算活动中最大价格
|
||||
|
||||
// 记录相关活动类型
|
||||
$skuPrice->activity_type = $activity['type'];
|
||||
$skuPrice->activity_id = $activity['id'];
|
||||
// 下单的时候需要存活动 的 sku_price_id)
|
||||
$skuPrice->item_goods_sku_price = $activitySkuPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $skuPrices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 这里要使用 shoproException 抛出异常
|
||||
*
|
||||
* @param array $buyInfo
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function buyCheck($buyInfo, $activity)
|
||||
{
|
||||
$buy_type = request()->param('buy_type', 'groupon');
|
||||
$groupon_id = request()->param('groupon_id', 0);
|
||||
// 拼团
|
||||
$rules = $activity['rules'];
|
||||
$is_alone = $rules['is_alone'] ?? 1;
|
||||
|
||||
$currentSkuPrice = $buyInfo['current_sku_price'];
|
||||
$is_leader_discount = $currentSkuPrice['is_leader_discount'];
|
||||
|
||||
// 成团人数
|
||||
$num = $rules['team_num'] ?? 1;
|
||||
// 额外需要的库存
|
||||
$need_add_num = 0;
|
||||
|
||||
// 要单独购买
|
||||
if ($buy_type == 'alone') {
|
||||
// 不允许单独购买
|
||||
if (!$is_alone) {
|
||||
throw new ShoproException('该商品不允许单独购买');
|
||||
}
|
||||
} else {
|
||||
// 拼团,临时将拼团价设置为商品价格
|
||||
if (!$groupon_id && $is_leader_discount) {
|
||||
// 开新团,并且有团长优惠,使用优惠价格
|
||||
$buyInfo['current_sku_price']['price'] = $currentSkuPrice['leader_price'];
|
||||
} else {
|
||||
// 参与团,或者没有团长优惠
|
||||
$buyInfo['current_sku_price']['price'] = $currentSkuPrice['groupon_price'];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是开新团
|
||||
if (!$groupon_id && $buy_type == 'groupon') {
|
||||
// 开团需要的最小库存
|
||||
$need_add_num = ($num - 1);
|
||||
}
|
||||
|
||||
// 当前库存,小于要购买的数量
|
||||
$need_num = $buyInfo['goods_num'] + ($need_add_num ?? 0);
|
||||
if ($currentSkuPrice['stock'] < $need_num) {
|
||||
if ($need_add_num && $is_alone && !$groupon_id && $buy_type == 'groupon') {
|
||||
throw new ShoproException('商品库存不足以开团,请选择单独购买');
|
||||
} else if ($buy_type == 'alone') {
|
||||
throw new ShoproException('商品库存不足');
|
||||
} else {
|
||||
throw new ShoproException('该商品不允商品库存不足以开团许单独购买');
|
||||
}
|
||||
}
|
||||
|
||||
$buyInfo['is_commission'] = $rules['is_commission'] ?? 0; // 是否参与分销
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function buy($buyInfo, $activity)
|
||||
{
|
||||
$user = auth_user();
|
||||
$buy_type = request()->param('buy_type', 'groupon');
|
||||
$groupon_id = request()->param('groupon_id', 0);
|
||||
|
||||
// 参与现有团
|
||||
if ($buy_type != 'alone' && $groupon_id) {
|
||||
// 检测并获取要参与的团
|
||||
$activityGroupon = $this->checkAndGetJoinGroupon($buyInfo, $user, $groupon_id);
|
||||
}
|
||||
|
||||
// 判断 并 增加 redis 销量
|
||||
$stockSale = new StockSale();
|
||||
$stockSale->cacheForwardSale($buyInfo);
|
||||
|
||||
// (开新团不判断)参与旧团 增加预拼团人数,上面加入团的时候已经判断过一次了,所以这里 99.99% 会加入成功的
|
||||
if (isset($activityGroupon) && $activityGroupon) {
|
||||
// 增加拼团预成员人数
|
||||
$goods = $buyInfo['goods'];
|
||||
$activity = $goods['activity'];
|
||||
$this->grouponCacheForwardNum($activityGroupon, $activity, $user);
|
||||
}
|
||||
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function buyOk($order, $user)
|
||||
{
|
||||
$this->joinGroupon($order, $user, function ($activityRules, $itemExt) {
|
||||
$team_num = $activityRules['team_num'] ?? 1;
|
||||
|
||||
return compact('team_num');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 拼团购买失败
|
||||
*
|
||||
* @param \think\Model $order
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function buyFail($order, $type)
|
||||
{
|
||||
if ($type == 'invalid') {
|
||||
if ($order->pay_mode == 'offline') {
|
||||
// 肯定是已经货到付款的订单取消订单,这时候已经添加了参团记录
|
||||
$this->refundGrouponLog($order);
|
||||
} else {
|
||||
// 订单失效,扣除预拼团人数(只处理正在进行中的团)
|
||||
$this->grouponCacheBackNum($order, $type);
|
||||
}
|
||||
} else {
|
||||
// type = refund 退款订单将参团标记为已退款
|
||||
$this->refundGrouponLog($order);
|
||||
}
|
||||
|
||||
// 判断扣除预销量 (活动信息还在 redis)
|
||||
$stockSale = new StockSale();
|
||||
$stockSale->cacheBackSale($order);
|
||||
}
|
||||
}
|
||||
320
addons/shopro/library/activity/provider/GrouponLadder.php
Normal file
320
addons/shopro/library/activity/provider/GrouponLadder.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
use addons\shopro\library\activity\traits\Groupon as GrouponTrait;
|
||||
use addons\shopro\service\StockSale;
|
||||
use addons\shopro\exception\ShoproException;
|
||||
|
||||
/**
|
||||
* 阶梯拼团
|
||||
*/
|
||||
class GrouponLadder extends Base
|
||||
{
|
||||
use GrouponTrait;
|
||||
|
||||
protected $rules = [
|
||||
"is_commission" => "require|boolean",
|
||||
"is_free_shipping" => "require|boolean",
|
||||
"sales_show_type" => "require",
|
||||
"ladders" => "require|array",
|
||||
"is_alone" => "require|boolean",
|
||||
"is_fictitious" => "require|boolean",
|
||||
"fictitious_time" => "requireIf:is_fictitious,1|float|egt:0",
|
||||
"is_team_card" => "require|boolean",
|
||||
"is_leader_discount" => "require|boolean",
|
||||
"valid_time" => "require|float|egt:0",
|
||||
"limit_num" => "number|egt:0",
|
||||
"refund_type" => "require", // 退款方式 back=原路退回|money=退回到余额
|
||||
"order_auto_close" => "float|egt:0",
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
'ladders.require' => '请填写拼团阶梯',
|
||||
'ladders.array' => '请填写拼团阶梯',
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"is_commission" => 0, // 是否参与分销
|
||||
"is_free_shipping" => 0, // 是否包邮
|
||||
"sales_show_type" => "real", // real=真实活动销量|goods=商品总销量(包含虚拟销量)
|
||||
"ladders" => [], // {ladder_one:2,ladder_two:2,ladder_three:3}
|
||||
"is_alone" => 0, // 是否允许单独购买
|
||||
"is_fictitious" => 0, // 是否允许虚拟成团
|
||||
"fictitious_num" => 0, // 最多虚拟人数 0:不允许虚拟 '' 不限制
|
||||
"fictitious_time" => 0, // 开团多长时间自动虚拟成团
|
||||
"is_team_card" => 0, // 参团卡显示
|
||||
"is_leader_discount" => 0, // 团长优惠
|
||||
"valid_time" => 0, // 组团有效时间, 0:一直有效
|
||||
"limit_num" => 0, // 每人限购数量 0:不限购
|
||||
"refund_type" => "back", // 退款方式 back=原路退回|money=退回到余额
|
||||
"order_auto_close" => 0, // 订单自动关闭时间,如果为 0 将使用系统级订单自动关闭时间
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 验证添加的活动商品是否至少设置了一个活动规格
|
||||
$this->checkActivitySkuPrice($params['goods_list']);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
public function save($activity, $params = [])
|
||||
{
|
||||
$goodsList = $params['goods_list'];
|
||||
|
||||
$this->saveSkuPrice($goodsList, $activity, function ($skuPrice) use ($activity) {
|
||||
// 处理 阶梯价格,团长优惠
|
||||
$rules = $activity->rules;
|
||||
$is_leader_discount = $rules['is_leader_discount'] ?? 0;
|
||||
$ladders = $rules['ladders'] ?? 0;
|
||||
|
||||
$ext = [
|
||||
'is_leader_discount' => $is_leader_discount,
|
||||
'ladders' => []
|
||||
];
|
||||
foreach ($ladders as $ladder_level => $ladder) {
|
||||
$ladder_price = isset($skuPrice[$ladder_level]) ? number_format(floatval($skuPrice[$ladder_level]), 2, '.', '') : 0;
|
||||
$leader_ladder_price = (isset($skuPrice[$ladder_level . '_leader']) && $skuPrice[$ladder_level . '_leader'] > 0) ? number_format(floatval($skuPrice[$ladder_level . '_leader']), 2, '.', '') : $ladder_price; // 默认当前阶梯参团价
|
||||
|
||||
$current = [
|
||||
'ladder_level' => $ladder_level,
|
||||
'ladder' => $ladder,
|
||||
'ladder_price' => $ladder_price,
|
||||
'leader_ladder_price' => $leader_ladder_price
|
||||
];
|
||||
unset($skuPrice[$ladder_level], $skuPrice[$ladder_level . '_leader']);
|
||||
$ext['ladders'][] = $current;
|
||||
}
|
||||
|
||||
$skuPrice['ext'] = $ext;
|
||||
return $skuPrice;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function showSkuPrice($skuPrice)
|
||||
{
|
||||
$ext = $skuPrice['ext'] ?? [];
|
||||
$ladders = $ext['ladders'] ?? [];
|
||||
|
||||
if ($ladders) {
|
||||
foreach ($ladders as $ladder) {
|
||||
$ladder_level = $ladder['ladder_level'];
|
||||
$skuPrice[$ladder_level] = $ladder['ladder_price'];
|
||||
$skuPrice[$ladder_level . '_leader'] = $ladder['leader_ladder_price'];
|
||||
}
|
||||
} else {
|
||||
// 全部初始化为 0
|
||||
$skuPrice['ladder_one'] = 0;
|
||||
$skuPrice['ladder_two'] = 0;
|
||||
$skuPrice['ladder_three'] = 0;
|
||||
$skuPrice['ladder_one_leader'] = 0;
|
||||
$skuPrice['ladder_two_leader'] = 0;
|
||||
$skuPrice['ladder_three_leader'] = 0;
|
||||
}
|
||||
|
||||
return $skuPrice;
|
||||
}
|
||||
|
||||
|
||||
public function recoverSkuPrices($goods, $activity)
|
||||
{
|
||||
$groupon_num = request()->param('groupon_num', 0); // 是否传了开团人数(这里不再使用阶梯,前端没反)
|
||||
$activitySkuPrices = $activity['activity_sku_prices'];
|
||||
$skuPrices = $goods->sku_prices;
|
||||
|
||||
foreach ($skuPrices as $key => &$skuPrice) {
|
||||
$stock = $skuPrice->stock; // 下面要用
|
||||
$skuPrice->stock = 0;
|
||||
$skuPrice->sales = 0;
|
||||
foreach ($activitySkuPrices as $activitySkuPrice) {
|
||||
if ($skuPrice['id'] == $activitySkuPrice['goods_sku_price_id']) {
|
||||
// 采用活动的 规格内容
|
||||
$is_leader_discount = $activitySkuPrice['ext']['is_leader_discount'];
|
||||
$ladders = $activitySkuPrice['ext']['ladders'];
|
||||
$skuPrice->old_price = $skuPrice->price; // 保存原始普通商品规格的价格(计算活动的优惠)
|
||||
$skuPrice->stock = ($activitySkuPrice['stock'] > $stock) ? $stock : $activitySkuPrice['stock']; // 活动库存不能超过商品库存
|
||||
$skuPrice->sales = $activitySkuPrice['sales'];
|
||||
$skuPrice->is_leader_discount = $is_leader_discount; // 是否团长优惠
|
||||
$skuPrice->ladders = $ladders; // 阶梯价格,包含团长优惠
|
||||
$skuPrice->status = $activitySkuPrice['status']; // 采用活动的上下架
|
||||
$skuPrice->ext = $activitySkuPrice['ext']; // 活动规格 ext, order_item 保存备用
|
||||
$skuPrice->min_price = min(array_column($ladders, 'ladder_price')); // 当前活动规格最小价格,这里是阶梯最低拼团价(不要团长价)
|
||||
$skuPrice->max_price = max(array_column($ladders, 'ladder_price')); // 当前活动规格最大价格,这里是阶梯最低拼团价(不要团长价)
|
||||
|
||||
$ladders = array_column($ladders, null, 'ladder');
|
||||
$currentLadder = $ladders[$groupon_num] ?? current($ladders);
|
||||
$skuPrice->ladder_price = $currentLadder['ladder_price']; // 当前阶梯价格(默认是 ladder_one)
|
||||
$skuPrice->leader_ladder_price = $currentLadder['leader_ladder_price']; // 当前阶梯团长价(默认是 ladder_one)
|
||||
$skuPrice->price = $is_leader_discount ? $skuPrice->leader_ladder_price : $skuPrice->ladder_price; // 默认是计算好的价格,团长价或者普通价
|
||||
|
||||
// 记录相关活动类型
|
||||
$skuPrice->activity_type = $activity['type'];
|
||||
$skuPrice->activity_id = $activity['id'];
|
||||
// 下单的时候需要存活动 的 sku_price_id)
|
||||
$skuPrice->item_goods_sku_price = $activitySkuPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $skuPrices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 这里要使用 shoproException 抛出异常
|
||||
*
|
||||
* @param array $buyInfo
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function buyCheck($buyInfo, $activity)
|
||||
{
|
||||
$buy_type = request()->param('buy_type', 'groupon');
|
||||
$groupon_id = request()->param('groupon_id', 0);
|
||||
$groupon_num = request()->param('groupon_num', 0);
|
||||
// 拼团
|
||||
$rules = $activity['rules'];
|
||||
$is_alone = $rules['is_alone'] ?? 1;
|
||||
|
||||
$currentSkuPrice = $buyInfo['current_sku_price'];
|
||||
$is_leader_discount = $currentSkuPrice['is_leader_discount']; // 是否团长优惠
|
||||
$ladders = $currentSkuPrice['ladders']; // 阶梯数据
|
||||
$ladders = array_column($ladders, null, 'ladder');
|
||||
$currentLadder = $ladders[$groupon_num] ?? current($ladders); // 当前阶梯的 价格数据
|
||||
|
||||
// 开新团,并且没有找到要参与的阶梯数据
|
||||
if (!$groupon_id && (!$currentLadder || $currentLadder['ladder'] <= 1)) {
|
||||
throw new ShoproException('请选择正确的开团阶梯');
|
||||
}
|
||||
|
||||
$buyInfo['ladder'] = $currentLadder; // 存储当前购买的拼团阶梯 ladder
|
||||
|
||||
// 额外需要的库存
|
||||
$need_add_num = 0;
|
||||
|
||||
// 要单独购买
|
||||
if ($buy_type == 'alone') {
|
||||
// 不允许单独购买
|
||||
if (!$is_alone) {
|
||||
throw new ShoproException('该商品不允许单独购买');
|
||||
}
|
||||
} else {
|
||||
// 拼团,临时将拼团价设置为商品价格
|
||||
if (!$groupon_id && $is_leader_discount) {
|
||||
// 开新团,并且有团长优惠,使用优惠价格
|
||||
$buyInfo['current_sku_price']['price'] = $currentLadder['leader_ladder_price'];
|
||||
} else {
|
||||
// 参与团,或者没有团长优惠
|
||||
$buyInfo['current_sku_price']['price'] = $currentSkuPrice['ladder_price'];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是开新团
|
||||
if (!$groupon_id && $buy_type == 'groupon') {
|
||||
// 成团人数
|
||||
$num = $currentLadder['ladder'] ?? 1;
|
||||
|
||||
// 开团需要的最小库存
|
||||
$need_add_num = ($num - 1);
|
||||
}
|
||||
|
||||
// 当前库存,小于要购买的数量
|
||||
$need_num = $buyInfo['goods_num'] + ($need_add_num ?? 0);
|
||||
if ($currentSkuPrice['stock'] < $need_num) {
|
||||
if ($need_add_num && $is_alone && !$groupon_id && $buy_type == 'groupon') {
|
||||
throw new ShoproException('商品库存不足以开团,请选择单独购买');
|
||||
} else if ($buy_type == 'alone') {
|
||||
throw new ShoproException('商品库存不足');
|
||||
} else {
|
||||
throw new ShoproException('商品库存不足以开团');
|
||||
}
|
||||
}
|
||||
|
||||
$buyInfo['is_commission'] = $rules['is_commission'] ?? 0; // 是否参与分销
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function buy($buyInfo, $activity)
|
||||
{
|
||||
$user = auth_user();
|
||||
$buy_type = request()->param('buy_type', 'groupon');
|
||||
$groupon_id = request()->param('groupon_id', 0);
|
||||
|
||||
// 参与现有团
|
||||
if ($buy_type != 'alone' && $groupon_id) {
|
||||
// 检测并获取要参与的团
|
||||
$activityGroupon = $this->checkAndGetJoinGroupon($buyInfo, $user, $groupon_id);
|
||||
}
|
||||
|
||||
// 判断 并 增加 redis 销量
|
||||
$stockSale = new StockSale();
|
||||
$stockSale->cacheForwardSale($buyInfo);
|
||||
|
||||
// (开新团不判断)参与旧团 增加预拼团人数,上面加入团的时候已经判断过一次了,所以这里 99.99% 会加入成功的
|
||||
if (isset($activityGroupon) && $activityGroupon) {
|
||||
// 增加拼团预成员人数
|
||||
$goods = $buyInfo['goods'];
|
||||
$activity = $goods['activity'];
|
||||
$this->grouponCacheForwardNum($activityGroupon, $activity, $user);
|
||||
}
|
||||
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function buyOk($order, $user)
|
||||
{
|
||||
$this->joinGroupon($order, $user, function ($activityRules, $itemExt) {
|
||||
// 处理拼团特殊的数据
|
||||
$ladder = $itemExt['ladder'];
|
||||
$team_num = $ladder['ladder'];
|
||||
|
||||
return compact('team_num');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 拼团购买失败
|
||||
*
|
||||
* @param \think\Model $order
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function buyFail($order, $type)
|
||||
{
|
||||
if ($type == 'invalid') {
|
||||
if ($order->pay_mode == 'offline') {
|
||||
// 肯定是已经货到付款的订单取消订单,这时候已经添加了参团记录
|
||||
$this->refundGrouponLog($order);
|
||||
} else {
|
||||
// 订单失效,扣除预拼团人数(只处理正在进行中的团)
|
||||
$this->grouponCacheBackNum($order, $type);
|
||||
}
|
||||
} else {
|
||||
// type = refund 退款订单将参团标记为已退款
|
||||
$this->refundGrouponLog($order);
|
||||
}
|
||||
|
||||
// 判断扣除预销量 (活动信息还在 redis)
|
||||
$stockSale = new StockSale();
|
||||
$stockSale->cacheBackSale($order);
|
||||
}
|
||||
}
|
||||
113
addons/shopro/library/activity/provider/GrouponLucky.php
Normal file
113
addons/shopro/library/activity/provider/GrouponLucky.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
/**
|
||||
* 幸运拼团
|
||||
*/
|
||||
class GrouponLucky extends Base
|
||||
{
|
||||
protected $rules = [
|
||||
// "is_commission" => "require|bool",
|
||||
// "is_free_shipping" => "require|bool",
|
||||
// "sales_show_type" => "require",
|
||||
// "team_num" => "require|number",
|
||||
// "lucky_num" => "require|number",
|
||||
// "is_fictitious" => "require|bool",
|
||||
// "fictitious_num" => "number|gt:0",
|
||||
// "fictitious_time" => "require|float|egt:0",
|
||||
// "part_gift" => "require|array",
|
||||
// "is_team_card" => "require|bool",
|
||||
// "is_leader_discount" => "require|bool",
|
||||
// "valid_time" => "require|float|gt:0",
|
||||
// "limit_num" => "number|gt:0",
|
||||
// "limit_team_buy" => "number|gt:0",
|
||||
// "refund_type" => "back", // 退款方式 back=原路退回|money=退回到余额
|
||||
// "order_auto_close" => "float|gt:0",
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
// 'team_num.require' => '请填写成团人数',
|
||||
// 'is_alone.require' => '请选择单独购买',
|
||||
// 'stock.gt' => '请填写补货数量'
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"is_commission" => 0, // 是否参与分销
|
||||
"is_free_shipping" => 0, // 是否包邮
|
||||
"sales_show_type" => "real", // real=真实活动销量|goods=商品总销量(包含虚拟销量)
|
||||
"team_num" => 2, // 成团人数,最少两人
|
||||
"lucky_num" => 1, // 拼中人数,最少一人
|
||||
"is_fictitious" => 0, // 是否允许虚拟成团
|
||||
"fictitious_num" => 0, // 最多虚拟人数
|
||||
"fictitious_time" => 0, // 开团多长时间自动虚拟成团
|
||||
"part_gift" => [], // {"types": "coupon=优惠券|score=积分|money=余额","coupon_ids":"赠优惠券时存在","total":"赠送优惠券总金额","score":"积分","money":"余额"}
|
||||
"is_team_card" => 0, // 参团卡显示
|
||||
"is_leader_discount" => 0, // 团长优惠
|
||||
"valid_time" => 0, // 组团有效时间
|
||||
"limit_num" => 0, // 每人限购数量
|
||||
"limit_team_buy" => 0, // 每人每团可参与次数
|
||||
"refund_type" => "back", // 退款方式 back=原路退回|money=退回到余额
|
||||
"order_auto_close" => 0, // 订单自动关闭时间
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 验证添加的活动商品是否至少设置了一个活动规格
|
||||
$this->checkActivitySkuPrice($params['goods_list']);
|
||||
|
||||
// 验证赠送规则字段
|
||||
$this->checkLuckyPartGift($params['rules']['part_gift']);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
public function save($activity, $params = [])
|
||||
{
|
||||
$goodsList = $params['goods_list'];
|
||||
|
||||
$this->saveSkuPrice($goodsList, $activity->id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function recoverSkuPrices($goods, $activity)
|
||||
{
|
||||
$activitySkuPrices = $activity['activity_sku_prices'];
|
||||
$skuPrices = $goods->sku_prices;
|
||||
|
||||
foreach ($skuPrices as $key => &$skuPrice) {
|
||||
$stock = $skuPrice->stock; // 下面要用
|
||||
$skuPrice->stock = 0;
|
||||
$skuPrice->sales = 0;
|
||||
foreach ($activitySkuPrices as $activitySkuPrice) {
|
||||
if ($skuPrice['id'] == $activitySkuPrice['goods_sku_price_id']) {
|
||||
// 采用活动的 规格内容
|
||||
$skuPrice->stock = ($activitySkuPrice['stock'] > $stock) ? $stock : $activitySkuPrice['stock']; // 活动库存不能超过商品库存
|
||||
$skuPrice->sales = $activitySkuPrice['sales'];
|
||||
$skuPrice->groupon_price = $activitySkuPrice['price']; // 不覆盖原来规格价格,用作单独购买,将活动的价格设置为新的拼团价格
|
||||
$skuPrice->status = $activitySkuPrice['status']; // 采用活动的上下架
|
||||
|
||||
// 记录相关活动类型
|
||||
$skuPrice->activity_type = $activity['type'];
|
||||
$skuPrice->activity_id = $activity['id'];
|
||||
// 下单的时候需要存活动 的 sku_price_id)
|
||||
$skuPrice->item_goods_sku_price = $activitySkuPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $skuPrice;
|
||||
}
|
||||
}
|
||||
151
addons/shopro/library/activity/provider/Seckill.php
Normal file
151
addons/shopro/library/activity/provider/Seckill.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
use addons\shopro\service\StockSale;
|
||||
use addons\shopro\exception\ShoproException;
|
||||
|
||||
/**
|
||||
* 秒杀
|
||||
*/
|
||||
class Seckill extends Base
|
||||
{
|
||||
protected $rules = [
|
||||
"is_commission" => "require|boolean",
|
||||
"is_free_shipping" => "require|boolean",
|
||||
"sales_show_type" => "require",
|
||||
"limit_num" => "number|egt:0",
|
||||
"order_auto_close" => "float|egt:0",
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"is_commission" => 0, // 是否参与分销
|
||||
"is_free_shipping" => 0, // 是否包邮
|
||||
"sales_show_type" => "real", // real=真实活动销量|goods=商品总销量(包含虚拟销量)
|
||||
"limit_num" => 0, // 每人限购数量 0:不限购
|
||||
"order_auto_close" => 0, // 订单自动关闭时间,如果为 0 将使用系统级订单自动关闭时间
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 验证添加的活动商品是否至少设置了一个活动规格
|
||||
$this->checkActivitySkuPrice($params['goods_list']);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, $params['goods_list'], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
public function save($activity, $params = [])
|
||||
{
|
||||
$goodsList = $params['goods_list'];
|
||||
|
||||
$this->saveSkuPrice($goodsList, $activity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function recoverSkuPrices($goods, $activity)
|
||||
{
|
||||
$activitySkuPrices = $activity['activity_sku_prices'];
|
||||
$skuPrices = $goods->sku_prices;
|
||||
|
||||
foreach ($skuPrices as $key => &$skuPrice) {
|
||||
$stock = $skuPrice->stock; // 下面要用
|
||||
$skuPrice->stock = 0;
|
||||
$skuPrice->sales = 0;
|
||||
foreach ($activitySkuPrices as $activitySkuPrice) {
|
||||
if ($skuPrice['id'] == $activitySkuPrice['goods_sku_price_id']) {
|
||||
// 采用活动的 规格内容
|
||||
$skuPrice->old_price = $skuPrice->price; // 保存原始普通商品规格的价格(计算活动的优惠)
|
||||
$skuPrice->stock = ($activitySkuPrice['stock'] > $stock) ? $stock : $activitySkuPrice['stock']; // 活动库存不能超过商品库存
|
||||
$skuPrice->sales = $activitySkuPrice['sales'];
|
||||
$skuPrice->price = $activitySkuPrice['price'];
|
||||
$skuPrice->status = $activitySkuPrice['status']; // 采用活动的上下架
|
||||
$skuPrice->min_price = $activitySkuPrice['price']; // 当前活动规格最小价格,这里是秒杀价
|
||||
$skuPrice->max_price = $activitySkuPrice['price']; // 用作计算活动中最大价格
|
||||
|
||||
// 记录相关活动类型
|
||||
$skuPrice->activity_type = $activity['type'];
|
||||
$skuPrice->activity_id = $activity['id'];
|
||||
// 下单的时候需要存活动 的 sku_price_id)
|
||||
$skuPrice->item_goods_sku_price = $activitySkuPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $skuPrices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 这里要使用 shoproException 抛出异常
|
||||
*
|
||||
* @param array $buyInfo
|
||||
* @param array $activity
|
||||
* @return array
|
||||
*/
|
||||
public function buyCheck($buyInfo, $activity)
|
||||
{
|
||||
// 秒杀
|
||||
$rules = $activity['rules'];
|
||||
|
||||
$currentSkuPrice = $buyInfo['current_sku_price'];
|
||||
|
||||
// 当前库存,小于要购买的数量
|
||||
$need_num = $buyInfo['goods_num'] + ($need_add_num ?? 0);
|
||||
if ($currentSkuPrice['stock'] < $need_num) {
|
||||
throw new ShoproException('商品库存不足');
|
||||
}
|
||||
|
||||
$buyInfo['is_commission'] = $rules['is_commission'] ?? 0; // 是否参与分销
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function buy($buyInfo, $activity)
|
||||
{
|
||||
$user = auth_user();
|
||||
|
||||
// 判断 并 增加 redis 销量
|
||||
$stockSale = new StockSale();
|
||||
$stockSale->cacheForwardSale($buyInfo);
|
||||
|
||||
return $buyInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function buyOk($order, $user)
|
||||
{
|
||||
// 不需要处理
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 购买失败
|
||||
*
|
||||
* @param array $order
|
||||
* @return void
|
||||
*/
|
||||
public function buyFail($order, $type)
|
||||
{
|
||||
// 判断扣除预销量 (活动信息还在 redis)
|
||||
$stockSale = new StockSale();
|
||||
$stockSale->cacheBackSale($order);
|
||||
}
|
||||
}
|
||||
51
addons/shopro/library/activity/provider/Signin.php
Normal file
51
addons/shopro/library/activity/provider/Signin.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\provider;
|
||||
|
||||
/**
|
||||
* 签到
|
||||
*/
|
||||
class Signin extends Base
|
||||
{
|
||||
|
||||
protected $rules = [
|
||||
"everyday" => "require",
|
||||
"is_inc" => "require|boolean",
|
||||
"inc_num" => "require",
|
||||
"until_day" => "require|egt:0",
|
||||
"discounts" => "array",
|
||||
"is_replenish" => "require|boolean",
|
||||
"replenish_days" => "require|gt:0",
|
||||
"replenish_limit" => "require|egt:0",
|
||||
"replenish_num" => "require|gt:0"
|
||||
];
|
||||
|
||||
|
||||
protected $message = [
|
||||
];
|
||||
|
||||
|
||||
protected $default = [
|
||||
"everyday" => 0, // 每日签到固定积分
|
||||
"is_inc" => 0, // 是否递增签到
|
||||
"inc_num" => 0, // 递增奖励
|
||||
"until_day" => 0, // 递增持续天数
|
||||
"discounts" => [], // 连续签到奖励 {full:5, value:10} // 可以为空
|
||||
"is_replenish" => 0, // 是否开启补签
|
||||
"replenish_days" => 1, // 可补签天数,最小 1
|
||||
"replenish_limit" => 0, // 补签时间限制,0 不限制
|
||||
"replenish_num" => 1, // 补签所消耗积分
|
||||
];
|
||||
|
||||
|
||||
public function check($params, $activity_id = 0)
|
||||
{
|
||||
// 数据验证
|
||||
$params = parent::check($params);
|
||||
|
||||
// 检测活动之间是否存在冲突
|
||||
$this->checkActivityConflict($params, [], $activity_id);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
576
addons/shopro/library/activity/traits/ActivityRedis.php
Normal file
576
addons/shopro/library/activity/traits/ActivityRedis.php
Normal file
@@ -0,0 +1,576 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\traits;
|
||||
|
||||
use addons\shopro\facade\Redis;
|
||||
use app\admin\model\shopro\activity\Activity;
|
||||
use think\helper\Str;
|
||||
|
||||
/**
|
||||
* 获取活动 redis 基础方法
|
||||
*/
|
||||
trait ActivityRedis
|
||||
{
|
||||
|
||||
protected $zsetKey = 'zset-activity'; // 活动集合 key
|
||||
protected $hashPrefix = 'hash-activity:'; // 活动前缀
|
||||
protected $hashGoodsPrefix = 'goods-'; // 活动中商品的前缀
|
||||
protected $hashGrouponPrefix = 'groupon-';
|
||||
|
||||
|
||||
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 获取活动相关信息 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
/**
|
||||
* 获取活动完整信息
|
||||
*
|
||||
* @param integer $id
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function getActivity($id, $type)
|
||||
{
|
||||
$keyActivity = $this->keyActivity($id, $type);
|
||||
|
||||
return $this->getActivityByKey($keyActivity);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过活动的键值,获取活动完整信息
|
||||
*
|
||||
* @param string $activityHashKey
|
||||
* @return array
|
||||
*/
|
||||
public function getActivityByKey($keyActivity)
|
||||
{
|
||||
// 取出整条 hash 记录
|
||||
$activity = Redis::HGETALL($keyActivity);
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除活动
|
||||
*
|
||||
* @param integer $id
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function delActivity($id, $type)
|
||||
{
|
||||
$keyActivity = $this->keyActivity($id, $type);
|
||||
|
||||
$this->delActivityByKey($keyActivity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 key 删除活动
|
||||
*
|
||||
* @param string $keyActivity
|
||||
* @return void
|
||||
*/
|
||||
public function delActivityByKey($keyActivity)
|
||||
{
|
||||
// 删除 hash
|
||||
Redis::DEL($keyActivity);
|
||||
|
||||
// 删除集合
|
||||
Redis::ZREM($this->zsetKey, $keyActivity);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取活动的状态
|
||||
*
|
||||
* @param string $keyActivity
|
||||
* @return string
|
||||
*/
|
||||
public function getActivityStatusByKey($keyActivity)
|
||||
{
|
||||
$prehead_time = Redis::HGET($keyActivity, 'prehead_time'); // 预热时间
|
||||
$start_time = Redis::HGET($keyActivity, 'start_time'); // 开始时间
|
||||
$end_time = Redis::HGET($keyActivity, 'end_time'); // 结束时间
|
||||
|
||||
// 获取活动状态
|
||||
$status = Activity::getStatusCode($prehead_time, $start_time, $end_time);
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 获取活动相关信息 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||
|
||||
|
||||
|
||||
|
||||
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 操作活动 hash ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
|
||||
/**
|
||||
* 计算每个规格的真实库存、销量
|
||||
*
|
||||
* @param array $goods
|
||||
* @param string $keyActivity
|
||||
* @return array
|
||||
*/
|
||||
private function calcGoods($goods, $keyActivity)
|
||||
{
|
||||
// 销量 key
|
||||
$keyActivityGoods = $this->keyActivityGoods($goods['goods_id'], $goods['goods_sku_price_id'], true);
|
||||
|
||||
// 缓存中的销量
|
||||
$cacheSale = Redis::HGET($keyActivity, $keyActivityGoods);
|
||||
|
||||
$stock = $goods['stock'] - $cacheSale;
|
||||
$goods['stock'] = $stock > 0 ? $stock : 0;
|
||||
$goods['sales'] = $cacheSale;
|
||||
|
||||
return $goods;
|
||||
}
|
||||
|
||||
|
||||
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 操作活动 hash ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||
|
||||
|
||||
|
||||
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 格式化活动内容 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
|
||||
/**
|
||||
* 格式化活动
|
||||
*
|
||||
* @param string array $keyActivity 活动 key 或者活动完整信息
|
||||
* @param string $type 格式化方式
|
||||
* @param array $data 额外参数
|
||||
* @return array
|
||||
*/
|
||||
public function formatActivityByKey($keyActivity, $type = 'normal', $data = [])
|
||||
{
|
||||
$activity = $this->{'formatActivity' . Str::studly($type)}($keyActivity, $data);
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 正常模式,只移除销量, 团信息,保留全部商品规格数据
|
||||
*
|
||||
* @param string|array $originalActivity
|
||||
* @param array $data 额外数据,商品 id
|
||||
* @return array|null
|
||||
*/
|
||||
public function formatActivityNormal($originalActivity, $data = [])
|
||||
{
|
||||
if (is_string($originalActivity)) {
|
||||
// 传入的是活动的key
|
||||
$keyActivity = $originalActivity;
|
||||
$originalActivity = $this->getActivityByKey($originalActivity);
|
||||
} else {
|
||||
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
|
||||
}
|
||||
|
||||
$activity = [];
|
||||
|
||||
foreach ($originalActivity as $key => $value) {
|
||||
// 包含 -sale 全部跳过
|
||||
if (strpos($key, '-sale') !== false) {
|
||||
continue;
|
||||
} else if (strpos($key, $this->hashGrouponPrefix) !== false) {
|
||||
// 拼团的参团人数,团用户,移除
|
||||
continue;
|
||||
} else if ($key == 'rules') {
|
||||
$activity[$key] = json_decode($value, true);
|
||||
} else {
|
||||
// 普通键值
|
||||
$activity[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($activity) {
|
||||
// 处理活动状态
|
||||
$activity['status'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
|
||||
$activity['status_text'] = Activity::getStatusText($activity['status']);
|
||||
}
|
||||
|
||||
return $activity ?: null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 简洁模式,只保留活动表基本信息
|
||||
*
|
||||
* @param string $originalActivity
|
||||
* @param array $data 额外数据,商品 id
|
||||
* @return array|null
|
||||
*/
|
||||
private function formatActivityClear($originalActivity, $data = [])
|
||||
{
|
||||
if (is_string($originalActivity)) {
|
||||
// 传入的是活动的key
|
||||
$keyActivity = $originalActivity;
|
||||
$originalActivity = $this->getActivityByKey($originalActivity);
|
||||
} else {
|
||||
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
|
||||
}
|
||||
|
||||
$activity = [];
|
||||
|
||||
foreach ($originalActivity as $key => $value) {
|
||||
// 包含 -sale 全部跳过
|
||||
if (strpos($key, $this->hashGoodsPrefix) !== false) {
|
||||
continue;
|
||||
} else if (strpos($key, $this->hashGrouponPrefix) !== false) {
|
||||
// 拼团的参团人数,团用户,移除
|
||||
continue;
|
||||
} else if ($key == 'rules') {
|
||||
$activity[$key] = json_decode($value, true);
|
||||
} else {
|
||||
// 普通键值
|
||||
$activity[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($activity) {
|
||||
// 处理活动状态
|
||||
$activity['status'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
|
||||
$activity['status_text'] = Activity::getStatusText($activity['status']);
|
||||
}
|
||||
|
||||
return $activity ?: null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取并按照商品展示格式化活动数据
|
||||
*
|
||||
* @param string $originalActivity hash key
|
||||
* @param array $data 额外数据,商品 id
|
||||
* @return array|null
|
||||
*/
|
||||
private function formatActivityGoods($originalActivity, $data = [])
|
||||
{
|
||||
$goods_id = $data['goods_id'] ?? 0;
|
||||
|
||||
if (is_string($originalActivity)) {
|
||||
// 传入的是活动的key
|
||||
$keyActivity = $originalActivity;
|
||||
$originalActivity = $this->getActivityByKey($originalActivity);
|
||||
} else {
|
||||
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
|
||||
}
|
||||
|
||||
$activity = [];
|
||||
|
||||
// 商品前缀
|
||||
$goodsPrefix = $this->hashGoodsPrefix . ($goods_id ? $goods_id . '-' : '');
|
||||
|
||||
foreach ($originalActivity as $key => $value) {
|
||||
// 包含 -sale 全部跳过
|
||||
if (strpos($key, '-sale') !== false) {
|
||||
continue;
|
||||
} else if (strpos($key, $goodsPrefix) !== false) {
|
||||
// 商品规格信息,或者特定商品规格信息
|
||||
$goods = json_decode($value, true);
|
||||
|
||||
// 计算销量库存数据
|
||||
$goods = $this->calcGoods($goods, $keyActivity);
|
||||
|
||||
// 商品规格项
|
||||
$activity['activity_sku_prices'][] = $goods;
|
||||
} else if ($goods_id && strpos($key, $this->hashGoodsPrefix) !== false) {
|
||||
// 需要特定商品时,移除别的非当前商品的数据
|
||||
continue;
|
||||
} else if (strpos($key, $this->hashGrouponPrefix) !== false) {
|
||||
// 拼团的参团人数,团用户,移除
|
||||
continue;
|
||||
} else if ($key == 'rules') {
|
||||
$activity[$key] = json_decode($value, true);
|
||||
} else {
|
||||
// 普通键值
|
||||
$activity[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($activity) {
|
||||
// 处理活动状态
|
||||
$activity['status'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
|
||||
$activity['status_text'] = Activity::getStatusText($activity['status']);
|
||||
}
|
||||
|
||||
return $activity ?: null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取并按照折扣格式展示格式化活动数据
|
||||
*
|
||||
* @param string $originalActivity hash key
|
||||
* @param array $data 额外数据
|
||||
* @return array|null
|
||||
*/
|
||||
public function formatActivityPromo($originalActivity, $data = [])
|
||||
{
|
||||
if (is_string($originalActivity)) {
|
||||
// 传入的是活动的key
|
||||
$keyActivity = $originalActivity;
|
||||
$originalActivity = $this->getActivityByKey($originalActivity);
|
||||
} else {
|
||||
$keyActivity = $this->keyActivity($originalActivity['id'], $originalActivity['type']);
|
||||
}
|
||||
|
||||
$activity = [];
|
||||
foreach ($originalActivity as $key => $value) {
|
||||
if ($key == 'rules') {
|
||||
$rules = json_decode($value, true);
|
||||
$activity[$key] = $rules;
|
||||
} else {
|
||||
// 普通键值
|
||||
$activity[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($activity) {
|
||||
// 处理活动状态
|
||||
$activity['status'] = Activity::getStatusCode($activity['prehead_time'], $activity['start_time'], $activity['end_time']);
|
||||
$activity['status_text'] = Activity::getStatusText($activity['status']);
|
||||
}
|
||||
|
||||
return $activity ?: null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 格式化活动内容 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 获取活动的 keys 数组 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有活动的 keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getKeysActivity()
|
||||
{
|
||||
// 获取活动集合
|
||||
$keysActivity = Redis::ZRANGE($this->zsetKey, 0, 999999999);
|
||||
|
||||
return $keysActivity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过活动 id 获取活动的 key(不知道活动类型,只知道 id 的时候用)
|
||||
*
|
||||
* @param integer $id
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyActivityById($id)
|
||||
{
|
||||
$keysActivity = $this->getKeysActivity();
|
||||
|
||||
foreach ($keysActivity as $keyActivity) {
|
||||
$suffix = ':' . $id;
|
||||
// 判断是否是要找的活动id, 截取 hashKey 后面几位,是否为当前要查找的活动 id
|
||||
if (substr($keyActivity, (strlen($keyActivity) - strlen($suffix))) == $suffix) {
|
||||
$currentKeyActivity = $keyActivity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $currentKeyActivity ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过活动 id 和 活动 type 获取 活动 key
|
||||
*
|
||||
* @param integer $activity_id
|
||||
* @param string $activity_type
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyActivityByIdType($activity_id, $activity_type)
|
||||
{
|
||||
$keyActivity = $this->keyActivity($activity_id, $activity_type);
|
||||
return $keyActivity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取对应活动类型的 活动 keys
|
||||
*
|
||||
* @param array $activityTypes
|
||||
* @param array|string $status 要查询的活动的状态
|
||||
* @return array
|
||||
*/
|
||||
public function getKeysActivityByTypes($activityTypes, $status = 'all')
|
||||
{
|
||||
$status = is_array($status) ? $status : [$status];
|
||||
|
||||
$activityTypes = is_array($activityTypes) ? $activityTypes : [$activityTypes];
|
||||
$activityTypes = array_values(array_filter(array_unique($activityTypes))); // 过滤空值
|
||||
|
||||
$keysActivity = $this->getKeysActivity();
|
||||
|
||||
// 获取对应的活动类型的集合
|
||||
$keysActivityTypes = [];
|
||||
foreach ($keysActivity as $keyActivity) {
|
||||
// 循环要查找的活动类型数组
|
||||
foreach ($activityTypes as $type) {
|
||||
$prefix = $this->hashPrefix . $type . ':';
|
||||
if (strpos($keyActivity, $prefix) === 0) { // 是要查找的类型
|
||||
$keysActivityTypes[] = $keyActivity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断活动状态
|
||||
if (!in_array('all', $status)) {
|
||||
foreach ($keysActivityTypes as $key => $keyActivity) {
|
||||
$activity_status = $this->getActivityStatusByKey($keyActivity);
|
||||
if (!in_array($activity_status, $status)) {
|
||||
unset($keysActivityTypes[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($keysActivityTypes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过商品获取该商品参与的活动的hash key
|
||||
*
|
||||
* @param integer $goods_id
|
||||
* @param Array $activityType
|
||||
* @param array|string $status 要查询的活动的状态
|
||||
* @return array
|
||||
*/
|
||||
private function getkeysActivityByGoods($goods_id, $activityType = [], $status = 'all')
|
||||
{
|
||||
// 获取对应类型的活动集合
|
||||
$keysActivity = $this->getKeysActivityByTypes($activityType, $status);
|
||||
|
||||
$keysActivityGoods = [];
|
||||
foreach ($keysActivity as $keyActivity) {
|
||||
// 判断这条活动是否包含该商品
|
||||
$goods_ids = array_filter(explode(',', Redis::HGET($keyActivity, 'goods_ids')));
|
||||
if (!in_array($goods_id, $goods_ids) && !empty($goods_ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$keysActivityGoods[] = $keyActivity;
|
||||
}
|
||||
|
||||
return $keysActivityGoods;
|
||||
}
|
||||
|
||||
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 获取活动的 keys 数组 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 获取活动相关 key ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||
|
||||
/**
|
||||
* 获取活动 hash 的 key
|
||||
*
|
||||
* @param integer $activity_id 活动 id
|
||||
* @param string $activity_type 活动类型
|
||||
* @return string
|
||||
*/
|
||||
private function keyActivity($activity_id, $activity_type)
|
||||
{
|
||||
// 示例 hash-activity:groupon:25
|
||||
return $this->hashPrefix . $activity_type . ':' . $activity_id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取活动 hash 中 商品相关的 key (is_sale 对应的活动商品的销量)
|
||||
*
|
||||
* @param integer $goods_id 商品
|
||||
* @param integer $sku_price_id 规格
|
||||
* @param boolean $is_sale 对应的活动商品的销量
|
||||
* @return string
|
||||
*/
|
||||
private function keyActivityGoods($goods_id, $sku_price_id, $is_sale = false)
|
||||
{
|
||||
// 示例 商品规格:goods-25-30 or 商品规格销量:goods-25-30-sale
|
||||
return $this->hashGoodsPrefix . $goods_id . '-' . $sku_price_id . ($is_sale ? '-sale' : '');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取活动中 拼团 的团数据的 key
|
||||
*
|
||||
* @param integer $groupon_id
|
||||
* @param integer $goods_id
|
||||
* @param string $type 空=团 key|num=团人数|users=团用户
|
||||
* @return string
|
||||
*/
|
||||
private function keyActivityGroupon($groupon_id, $goods_id, $type = '')
|
||||
{
|
||||
return $this->hashGrouponPrefix . $groupon_id . '-' . $goods_id . ($type ? '-' . $type : '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取活动相关的所有 key
|
||||
*
|
||||
* @param array $detail 商品相关数据
|
||||
* @param array $activity 活动相关数据
|
||||
* @return array
|
||||
*/
|
||||
public function keysActivity($detail, $activity)
|
||||
{
|
||||
// 获取 hash key
|
||||
$keyActivity = $this->keyActivity($activity['activity_id'], $activity['activity_type']);
|
||||
|
||||
$keyGoodsSkuPrice = '';
|
||||
$keySale = '';
|
||||
if (isset($detail['goods_sku_price_id']) && $detail['goods_sku_price_id']) {
|
||||
// 获取 hash 表中商品 sku 的 key
|
||||
$keyGoodsSkuPrice = $this->keyActivityGoods($detail['goods_id'], $detail['goods_sku_price_id']);
|
||||
// 获取 hash 表中商品 sku 的 销量的 key
|
||||
$keySale = $this->keyActivityGoods($detail['goods_id'], $detail['goods_sku_price_id'], true);
|
||||
}
|
||||
|
||||
// 需要拼团的字段
|
||||
$keyGroupon = '';
|
||||
$keyGrouponNum = '';
|
||||
$keyGrouponUserlist = '';
|
||||
if (isset($detail['groupon_id']) && $detail['groupon_id']) {
|
||||
// 获取 hash 表中团 key
|
||||
$keyGroupon = $this->keyActivityGroupon($detail['groupon_id'], $detail['goods_id']);
|
||||
// 获取 hash 表中团当前人数 key
|
||||
$keyGrouponNum = $this->keyActivityGroupon($detail['groupon_id'], $detail['goods_id'], 'num');
|
||||
// 获取 hash 表中团当前人员列表 key
|
||||
$keyGrouponUserlist = $this->keyActivityGroupon($detail['groupon_id'], $detail['goods_id'], 'users');
|
||||
}
|
||||
|
||||
return compact('keyActivity', 'keyGoodsSkuPrice', 'keySale', 'keyGroupon', 'keyGrouponNum', 'keyGrouponUserlist');
|
||||
}
|
||||
|
||||
|
||||
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ 获取活动相关 key ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||
}
|
||||
160
addons/shopro/library/activity/traits/CheckActivity.php
Normal file
160
addons/shopro/library/activity/traits/CheckActivity.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\traits;
|
||||
|
||||
trait CheckActivity
|
||||
{
|
||||
|
||||
/**
|
||||
* 检测活动商品必须至少设置一个规格为活动规格
|
||||
*
|
||||
* @param array $goodsList
|
||||
* @return void
|
||||
*/
|
||||
public function checkActivitySkuPrice($goodsList)
|
||||
{
|
||||
foreach ($goodsList as $key => $goods) {
|
||||
$activitySkuPrice = $goods['activity_sku_prices'] ?? [];
|
||||
if (!$activitySkuPrice) {
|
||||
error_stop('请至少将商品一个规格设置为活动规格');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检测满赠规则设置
|
||||
*
|
||||
* @param array $discounts
|
||||
* @return void
|
||||
*/
|
||||
public function checkGiftDiscount($discounts)
|
||||
{
|
||||
foreach ($discounts as $discount) {
|
||||
$types = $discount['types'];
|
||||
|
||||
if (in_array('coupon', $types) && (!isset($discount['coupon_ids']) || empty($discount['coupon_ids']))) {
|
||||
// 验证优惠券
|
||||
error_stop('请选择要赠送的优惠券');
|
||||
}
|
||||
if (in_array('money', $types) && (!isset($discount['money']) || empty($discount['money']))) {
|
||||
// 赠送余额
|
||||
error_stop('请填写要赠送的余额');
|
||||
}
|
||||
if (in_array('score', $types) && (!isset($discount['score']) || empty($discount['score']))) {
|
||||
// 赠送积分
|
||||
error_stop('请填写要赠送的积分');
|
||||
}
|
||||
|
||||
if (in_array('goods', $types) && (!isset($discount['goods_ids']) || empty($discount['goods_ids']))) {
|
||||
// 赠送优惠券
|
||||
error_stop('请选择要赠送的商品');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 幸运拼团参与奖校验
|
||||
*
|
||||
* @param array $part_gift
|
||||
* @return void
|
||||
*/
|
||||
public function checkLuckyPartGift($part_gift)
|
||||
{
|
||||
$types = $part_gift['types'];
|
||||
|
||||
if (in_array('coupon', $types) && (!isset($part_gift['coupon_ids']) || empty($part_gift['coupon_ids']))) {
|
||||
// 验证优惠券
|
||||
error_stop('请选择要赠送的优惠券');
|
||||
}
|
||||
if (in_array('money', $types) && (!isset($discount['money']) || empty($discount['money']))) {
|
||||
// 赠送余额
|
||||
error_stop('请填写要赠送的余额');
|
||||
}
|
||||
if (in_array('score', $types) && (!isset($discount['score']) || empty($discount['score']))) {
|
||||
// 赠送积分
|
||||
error_stop('请填写要赠送的积分');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检测活动商品是否重合
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function checkActivityConflict($params, $goodsList = [], $activity_id = 0)
|
||||
{
|
||||
if ($params['classify'] == 'activity') {
|
||||
// 活动可以共存,不检测冲突
|
||||
return true;
|
||||
}
|
||||
$start_time = strtotime($params['start_time']);
|
||||
$end_time = strtotime($params['end_time']);
|
||||
$prehead_time = isset($params['prehead_time']) && $params['prehead_time'] ? strtotime($params['prehead_time']) : $start_time;
|
||||
$goodsIds = array_column($goodsList, 'id'); // 获取活动提交过来的所有商品的 id
|
||||
$goodsList = array_column($goodsList, null, 'id');
|
||||
|
||||
// 获取所有时间有交叉的活动
|
||||
$activities = $this->getActivities($params['type'], [$prehead_time, $end_time]);
|
||||
|
||||
foreach ($activities as $key => $activity) {
|
||||
if ($activity_id && $activity_id == $activity['id']) {
|
||||
// 编辑的时候,把自己排除在外
|
||||
continue;
|
||||
}
|
||||
|
||||
$intersect = []; // 两个活动重合的商品Ids
|
||||
if ($goodsIds) {
|
||||
$activityGoodsIds = array_filter(explode(',', $activity['goods_ids']));
|
||||
// 不是全部商品,并且不重合
|
||||
if ($activityGoodsIds && !$intersect = array_intersect($activityGoodsIds, $goodsIds)) {
|
||||
// 商品不重合,继续验证下个活动
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$goods_names = '';
|
||||
foreach ($intersect as $id) {
|
||||
if (isset($goodsList[$id]) && isset($goodsList[$id]['title'])) {
|
||||
$goods_names .= $goodsList[$id]['title'] . ',';
|
||||
}
|
||||
}
|
||||
|
||||
if ($goods_names) {
|
||||
$goods_names = mb_strlen($goods_names) > 40 ? mb_substr($goods_names, 0, 37) . '...' : $goods_names;
|
||||
}
|
||||
|
||||
if (!$goodsIds && !$intersect) {
|
||||
// 没有商品
|
||||
$msg = '活动时间与 “' . $activity['type_text'] . ' 活动的 ' . $activity['title'] . '” 冲突';
|
||||
} else {
|
||||
$msg = ((count($intersect) > 1 || !$goodsIds) ? '部分商品' : '该商品') . ($goods_names ? ' ' . $goods_names . ' ' : '') . '已在 “' . $activity['title'] . '” 活动中设置';
|
||||
}
|
||||
|
||||
error_stop($msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有活动
|
||||
*
|
||||
* @param string $current_activity_type
|
||||
* @param array $range 要查询的时间区间
|
||||
* @return array
|
||||
*/
|
||||
private function getActivities($current_activity_type, $range) {
|
||||
// 获取当前活动的互斥活动
|
||||
$activityTypes = $this->manager->model->getMutexActivityTypes($current_activity_type);
|
||||
|
||||
$activities = $this->manager->getActivitiesByRange($range, $activityTypes);
|
||||
|
||||
return $activities;
|
||||
}
|
||||
}
|
||||
269
addons/shopro/library/activity/traits/GiveGift.php
Normal file
269
addons/shopro/library/activity/traits/GiveGift.php
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\traits;
|
||||
|
||||
use think\Db;
|
||||
use addons\shopro\traits\CouponSend;
|
||||
use app\admin\model\shopro\activity\GiftLog;
|
||||
use addons\shopro\service\Wallet as WalletService;
|
||||
|
||||
/**
|
||||
* 赠送赠品 (full_gift, groupon_lucky 幸运拼团未拼中)
|
||||
*/
|
||||
trait GiveGift
|
||||
{
|
||||
use CouponSend;
|
||||
|
||||
/**
|
||||
* 按照规则添加赠送日志
|
||||
* @param array|object $order
|
||||
* @param array|object $user
|
||||
* @param array $info {"full":"100","types":"coupon=优惠券|score=积分|money=余额|goods=商品","coupon_ids":"赠优惠券时存在","total":"赠送优惠券总金额","score":"积分","money":"余额","goods_ids":"商品时存在",gift_num:"礼品份数"}
|
||||
* @return void
|
||||
*/
|
||||
public function addGiftsLog($order, $user, $info)
|
||||
{
|
||||
$rules = $info['discount_rule'];
|
||||
|
||||
Db::transaction(function () use ($order, $user, $info, $rules) {
|
||||
$types = $rules['types'];
|
||||
|
||||
foreach ($types as $type) {
|
||||
extract($this->getTypeGift($rules, $type));
|
||||
|
||||
$giftLog = new GiftLog();
|
||||
$giftLog->activity_id = $info['activity_id'];
|
||||
$giftLog->order_id = $order->id;
|
||||
$giftLog->user_id = $user->id;
|
||||
$giftLog->type = $type;
|
||||
$giftLog->gift = $gift;
|
||||
$giftLog->value = $value;
|
||||
$giftLog->rules = $rules;
|
||||
$giftLog->status = 'waiting';
|
||||
$giftLog->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 标记礼品为赠送失败
|
||||
*/
|
||||
public function checkAndFailGift($order, $fail_msg, $errors = null)
|
||||
{
|
||||
// 找到所有没有赠送的礼品,设置为 fail,fail_msg 订单退款
|
||||
$giftLogs = GiftLog::waiting()->where('order_id', $order->id)->lock(true)->select();
|
||||
foreach ($giftLogs as $giftLog) {
|
||||
$giftLog->status = 'fail';
|
||||
$giftLog->fail_msg = $fail_msg;
|
||||
$giftLog->errors = $errors;
|
||||
$giftLog->save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查并赠送礼品
|
||||
*
|
||||
* @param array|object $order
|
||||
* @param array|object $user
|
||||
* @param array $promoInfos
|
||||
* @param string $event
|
||||
* @return void
|
||||
*/
|
||||
public function checkAndGift($order, $user, $promoInfos, $event)
|
||||
{
|
||||
foreach ($promoInfos as $info) {
|
||||
if ($info['activity_type'] == 'full_gift') {
|
||||
$this->checkPromoAndGift($order, $user, $info, $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查并赠送礼品
|
||||
*
|
||||
* @param array|object $order
|
||||
* @param array|object $user
|
||||
* @param array $infos
|
||||
* @param string $event
|
||||
* @return void
|
||||
*/
|
||||
public function checkPromoAndGift($order, $user, $info, $event)
|
||||
{
|
||||
if ($info['event'] == $event) {
|
||||
// 判断领取次数
|
||||
$rules = $info['discount_rule'];
|
||||
$gift_num = $rules['gift_num']; // 礼品数量
|
||||
|
||||
// 查询已发放数量
|
||||
$send_num = GiftLog::where('activity_id', $info['activity_id'])->opered()->group('order_id')->count();
|
||||
|
||||
$giftLogs = GiftLog::waiting()
|
||||
->where('activity_id', $info['activity_id'])
|
||||
->where('order_id', $order->id)
|
||||
->select();
|
||||
|
||||
if ($send_num >= $gift_num) {
|
||||
// 礼品已经发放完毕
|
||||
foreach ($giftLogs as $log) {
|
||||
$log->status = 'fail';
|
||||
$log->fail_msg = '礼品已经发完了';
|
||||
$log->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查询当前用户已领取数量 (只算赠送成功的)
|
||||
$user_send_num = GiftLog::where('user_id', $order->user_id)->where('activity_id', $info['activity_id'])->finish()->group('order_id')->count();
|
||||
if ($info['limit_num'] > 0 && $user_send_num >= $info['limit_num']) {
|
||||
// 已经领取过了
|
||||
foreach ($giftLogs as $log) {
|
||||
$log->status = 'fail';
|
||||
$log->fail_msg = '已经领取过了,每人最多领取 ' . $info['limit_num'] . ' 份';
|
||||
$log->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 赠送礼品
|
||||
foreach ($giftLogs as $giftLog) {
|
||||
$this->{'gift' . $giftLog->type}($user, $giftLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 赠送优惠券
|
||||
*
|
||||
* @param array|object $user
|
||||
* @param array|object $giftLog
|
||||
* @return void
|
||||
*/
|
||||
public function giftCoupon($user, $giftLog)
|
||||
{
|
||||
$couponIds = explode(',', $giftLog->gift);
|
||||
|
||||
$result = $this->giveCoupons($user, $couponIds);
|
||||
|
||||
$giftLog->status = 'finish';
|
||||
if ($result['errors']) {
|
||||
$giftLog->status = 'fail';
|
||||
$giftLog->fail_msg = $result['success'] ? '优惠券部分发放成功' : '优惠券发放失败';
|
||||
$giftLog->errors = $result['errors'];
|
||||
}
|
||||
|
||||
$giftLog->save();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 赠送积分
|
||||
*
|
||||
* @param array|object $user
|
||||
* @param array|object $giftLog
|
||||
* @return void
|
||||
*/
|
||||
public function giftScore($user, $giftLog)
|
||||
{
|
||||
$score = $giftLog->gift;
|
||||
|
||||
// 增加用户积分
|
||||
WalletService::change($user, 'score', $score, 'activity_gift', [
|
||||
'activity_id' => $giftLog->activity_id,
|
||||
'order_id' => $giftLog->order_id,
|
||||
'user_id' => $giftLog->user_id,
|
||||
'type' => $giftLog->type,
|
||||
'gift' => $giftLog->gift,
|
||||
'value' => $giftLog->value,
|
||||
]);
|
||||
|
||||
$giftLog->status = 'finish';
|
||||
$giftLog->save();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 赠送余额
|
||||
*
|
||||
* @param array|object $user
|
||||
* @param array|object $giftLog
|
||||
* @return void
|
||||
*/
|
||||
public function giftMoney($user, $giftLog)
|
||||
{
|
||||
$money = $giftLog->gift;
|
||||
|
||||
// 增加用户余额
|
||||
WalletService::change($user, 'money', $money, 'activity_gift', [
|
||||
'activity_id' => $giftLog->activity_id,
|
||||
'order_id' => $giftLog->order_id,
|
||||
'user_id' => $giftLog->user_id,
|
||||
'type' => $giftLog->type,
|
||||
'gift' => $giftLog->gift,
|
||||
'value' => $giftLog->value,
|
||||
]);
|
||||
|
||||
$giftLog->status = 'finish';
|
||||
$giftLog->save();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 赠送商品(暂不开发)
|
||||
*
|
||||
* @param array|object $user
|
||||
* @param array|object $giftLog
|
||||
* @return void
|
||||
*/
|
||||
public function giftGoods($user, $giftLog)
|
||||
{
|
||||
$goodsIds = explode(',', $giftLog->gift);
|
||||
|
||||
// 赠送商品,暂不开发
|
||||
$giftLog->status = 'finish';
|
||||
$giftLog->save();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取赠送的 gift 和价值
|
||||
*
|
||||
* @param array $rules
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
private function getTypeGift($rules, $type)
|
||||
{
|
||||
$gift = null;
|
||||
switch ($type) {
|
||||
case 'coupon':
|
||||
$gift = $rules['coupon_ids'];
|
||||
$value = $rules['total'];
|
||||
break;
|
||||
case 'score':
|
||||
$gift = $rules['score'];
|
||||
$value = $rules['score'];
|
||||
break;
|
||||
case 'money':
|
||||
$gift = $rules['money'];
|
||||
$value = $rules['money'];
|
||||
break;
|
||||
case 'goods':
|
||||
$gift = $rules['goods_ids'];
|
||||
$value = $rules['goods_ids'];
|
||||
break;
|
||||
}
|
||||
|
||||
return compact('gift', 'value');
|
||||
}
|
||||
}
|
||||
527
addons/shopro/library/activity/traits/Groupon.php
Normal file
527
addons/shopro/library/activity/traits/Groupon.php
Normal file
@@ -0,0 +1,527 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\activity\traits;
|
||||
|
||||
use addons\shopro\facade\Redis;
|
||||
use addons\shopro\facade\ActivityRedis;
|
||||
use app\admin\model\shopro\activity\Activity;
|
||||
use app\admin\model\shopro\activity\Groupon as ActivityGroupon;
|
||||
use app\admin\model\shopro\activity\GrouponLog;
|
||||
use app\admin\model\shopro\order\Order;
|
||||
use app\admin\model\shopro\order\OrderItem;
|
||||
use app\admin\model\shopro\data\FakeUser;
|
||||
use addons\shopro\service\order\OrderRefund;
|
||||
use addons\shopro\service\order\OrderOper;
|
||||
|
||||
/**
|
||||
* 拼团 (普通拼团,阶梯拼团,幸运拼团)
|
||||
*/
|
||||
trait Groupon
|
||||
{
|
||||
/**
|
||||
* *、redis 没有存团完整信息,只存了团当前人数,团成员(当前人数,团成员均没有存虚拟用户)
|
||||
* *、redis userList 没有存这个人的购买状态
|
||||
* *、团 解散,成团,(因为直接修改了数据库,参团判断,先判断的数据库后判断的 redis)
|
||||
* *、虚拟成团时将虚拟人数存入 redis userList 中,因为团中有虚拟人时,redis 实际人数 和 团需要人数 都没有计算虚拟人,导致团可以超员
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 判断加入旧拼团
|
||||
*/
|
||||
protected function checkAndGetJoinGroupon($buyInfo, $user, $groupon_id)
|
||||
{
|
||||
$goods = $buyInfo['goods'];
|
||||
$activity = $goods['activity'];
|
||||
|
||||
// 获取团信息
|
||||
$activityGroupon = ActivityGroupon::where('id', $groupon_id)->find();
|
||||
if (!$activityGroupon) {
|
||||
error_stop('要参与的团不存在');
|
||||
}
|
||||
// 判断团所属活动是否正常
|
||||
if ($activityGroupon->activity_id != $activity['id']) { // 修复,后台手动将活动删除,然后又立即给这个商品创建新的拼团活动,导致参与新活动的旧团错乱问题
|
||||
error_stop('要参与的活动已结束');
|
||||
}
|
||||
if ($activityGroupon['status'] != 'ing') {
|
||||
error_stop('要参与的团已成团,请选择其它团或自己开团');
|
||||
}
|
||||
|
||||
if ($activityGroupon['current_num'] >= $activityGroupon['num']) {
|
||||
error_stop('该团已满,请参与其它团或自己开团');
|
||||
}
|
||||
|
||||
if (!has_redis()) {
|
||||
// 没有 redis 直接判断数据库团信息,因为 current_num 支付成功才会累加,故无法保证超员,
|
||||
$isJoin = GrouponLog::where('user_id', $user['id'])->where('groupon_id', $activityGroupon->id)->where('is_fictitious', 0)->count();
|
||||
if ($isJoin) {
|
||||
error_stop('您已参与该团,请不要重复参团');
|
||||
}
|
||||
|
||||
// 该团可加入
|
||||
return $activityGroupon;
|
||||
}
|
||||
|
||||
$keys = ActivityRedis::keysActivity([
|
||||
'groupon_id' => $activityGroupon['id'],
|
||||
'goods_id' => $activityGroupon['goods_id'],
|
||||
], [
|
||||
'activity_id' => $activity['id'],
|
||||
'activity_type' => $activity['type'],
|
||||
]);
|
||||
|
||||
extract($keys);
|
||||
|
||||
$current_num = Redis::HGET($keyActivity, $keyGrouponNum);
|
||||
if ($current_num >= $activityGroupon['num']) {
|
||||
error_stop('该团已满,请参与其它团或自己开团');
|
||||
}
|
||||
|
||||
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,订单失效时删除缓存
|
||||
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
|
||||
$userList = json_decode($userList, true);
|
||||
$userIds = array_column($userList, 'user_id');
|
||||
if (in_array($user['id'], $userIds)) {
|
||||
error_stop('您已参与该团,请不要重复参团');
|
||||
}
|
||||
|
||||
return $activityGroupon;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 增加拼团预成员人数
|
||||
*/
|
||||
protected function grouponCacheForwardNum($activityGroupon, $activity, $user, $payed = 'nopay')
|
||||
{
|
||||
if (!has_redis()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$keys = ActivityRedis::keysActivity([
|
||||
'groupon_id' => $activityGroupon['id'],
|
||||
'goods_id' => $activityGroupon['goods_id'],
|
||||
], [
|
||||
'activity_id' => $activity['id'],
|
||||
'activity_type' => $activity['type'],
|
||||
]);
|
||||
|
||||
extract($keys);
|
||||
|
||||
// 当前团人数 grouponNumKey 如果不存在,自动创建
|
||||
$current_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, 1);
|
||||
|
||||
if ($current_num > $activityGroupon['num']) {
|
||||
// 再把刚加上的减回来
|
||||
$current_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, -1);
|
||||
|
||||
error_stop('该团已满,请参与其它团或自己开团');
|
||||
}
|
||||
|
||||
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消失效订单时删除缓存
|
||||
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
|
||||
$userList = json_decode($userList, true);
|
||||
$userList = $userList ?: [];
|
||||
$userList[] = [
|
||||
'user_id' => $user['id'],
|
||||
// 'status' => $payed // 太复杂,先不做
|
||||
];
|
||||
Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 拼团团成员预成员退回
|
||||
protected function grouponCacheBackNum($order, $type)
|
||||
{
|
||||
if (!has_redis()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 查询拼团商品
|
||||
$item = OrderItem::where('order_id', $order['id'])->find(); // 拼团订单只有一个商品
|
||||
|
||||
// 扩展字段
|
||||
$order_ext = $order['ext'];
|
||||
// 团 id
|
||||
$groupon_id = $order_ext['groupon_id'] ?? 0;
|
||||
|
||||
if (!$groupon_id) {
|
||||
return true; // 商品独立购买,未参团,或者开新团
|
||||
}
|
||||
|
||||
// 查询拼团,必须是拼团中才处理(已结束的(完成或者解散的没意义了)),redis 中没有存 团信息和状态
|
||||
$groupon = ActivityGroupon::ing()->lock(true)->find($groupon_id);
|
||||
if (!$groupon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if ($type == 'refund') { // 退款这里不删除拼团记录,当成正常团成员处理
|
||||
// // 退款,真实删除拼团记录,并减少参团人数
|
||||
// $this->delGrouponLog($order, $groupon);
|
||||
// }
|
||||
|
||||
$keys = ActivityRedis::keysActivity([
|
||||
'groupon_id' => $groupon_id,
|
||||
'goods_id' => $item['goods_id'],
|
||||
'goods_sku_price_id' => $item['goods_sku_price_id'],
|
||||
], [
|
||||
'activity_id' => $item['activity_id'],
|
||||
'activity_type' => $item['activity_type'],
|
||||
]);
|
||||
|
||||
extract($keys);
|
||||
|
||||
if (!Redis::EXISTS($keyActivity)) {
|
||||
// redis 不存在,可能活动已删除,不处理
|
||||
return true;
|
||||
}
|
||||
|
||||
// 扣除预参团成员
|
||||
if (Redis::HEXISTS($keyActivity, $keyGrouponNum)) {
|
||||
$groupon_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, -1);
|
||||
}
|
||||
|
||||
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
|
||||
$userList = json_decode($userList, true);
|
||||
$userList = $userList ?: [];
|
||||
foreach ($userList as $key => $user) {
|
||||
if ($user['user_id'] == $item['user_id']) {
|
||||
unset($userList[$key]);
|
||||
}
|
||||
}
|
||||
$userList = array_values($userList);
|
||||
Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 支付成功真实加入团
|
||||
*/
|
||||
protected function joinGroupon($order, $user, \Closure $grouponCb = null)
|
||||
{
|
||||
$items = $order->items;
|
||||
$item = $items[0]; // 拼团只能单独购买
|
||||
|
||||
// 扩展字段
|
||||
$order_ext = $order['ext'];
|
||||
// 团 id
|
||||
$groupon_id = $order_ext['groupon_id'] ?? 0;
|
||||
$buy_type = $order_ext['buy_type'] ?? 'groupon';
|
||||
|
||||
// 单独购买,不加入团
|
||||
if ($buy_type == 'alone') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($groupon_id) {
|
||||
// 加入旧团,查询团
|
||||
$activityGroupon = ActivityGroupon::find($groupon_id);
|
||||
} else {
|
||||
// 加入新团,创建团
|
||||
$activityGroupon = $this->joinNewGroupon($order, $user, $item, $grouponCb);
|
||||
}
|
||||
// 添加参团记录
|
||||
$activityGrouponLog = $this->addGrouponLog($order, $user, $item, $activityGroupon);
|
||||
|
||||
return $this->checkGrouponStatus($activityGroupon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 支付成功开启新拼团
|
||||
*/
|
||||
protected function joinNewGroupon($order, $user, $item, \Closure $grouponCb = null)
|
||||
{
|
||||
// 获取活动
|
||||
$activity = Activity::where('id', $item['activity_id'])->find();
|
||||
$rules = $activity['rules'];
|
||||
|
||||
// 小于 0 不限结束时间单位小时
|
||||
$expire_time = 0;
|
||||
if (isset($rules['valid_time']) && $rules['valid_time'] > 0) {
|
||||
// 转为 秒
|
||||
$expire_time = $rules['valid_time'] * 3600;
|
||||
}
|
||||
|
||||
// 小于 0 不限结束时间单位小时
|
||||
$fictitious_time = 0;
|
||||
if (isset($rules['is_fictitious']) && $rules['is_fictitious'] && isset($rules['fictitious_time']) && $rules['fictitious_time'] > 0) {
|
||||
// 转为 秒
|
||||
$fictitious_time = $rules['fictitious_time'] * 3600;
|
||||
}
|
||||
|
||||
if ($grouponCb) {
|
||||
// team_num
|
||||
extract($grouponCb($rules, $item['ext']));
|
||||
}
|
||||
|
||||
// 开团
|
||||
$activityGroupon = new ActivityGroupon();
|
||||
$activityGroupon->user_id = $user['id'];
|
||||
$activityGroupon->goods_id = $item['goods_id'];
|
||||
$activityGroupon->activity_id = $item['activity_id'];
|
||||
$activityGroupon->num = $team_num ?? 1; // 避免活动找不到
|
||||
$activityGroupon->current_num = 0; // 真实团成员等支付完成之后再增加
|
||||
$activityGroupon->status = 'ing';
|
||||
$activityGroupon->expire_time = $expire_time > 0 ? (time() + $expire_time) : 0;
|
||||
$activityGroupon->save();
|
||||
|
||||
// 记录团 id
|
||||
$order->ext = array_merge($order->ext, ['groupon_id' => $activityGroupon->id]);
|
||||
$order->save();
|
||||
|
||||
// 将团信息存入缓存,增加缓存中当前团人数
|
||||
$this->grouponCacheForwardNum($activityGroupon, $activity, $user, 'payed');
|
||||
|
||||
if ($expire_time > 0) {
|
||||
// 增加自动关闭拼团队列(如果有虚拟成团,会判断虚拟成团)
|
||||
\think\Queue::later($expire_time, '\addons\shopro\job\GrouponAutoOper@expire', [
|
||||
'activity' => $activity,
|
||||
'activity_groupon_id' => $activityGroupon->id
|
||||
], 'shopro');
|
||||
}
|
||||
|
||||
if ($fictitious_time > 0) {
|
||||
// 自动虚拟成团时间(提前自动虚拟成团,让虚拟成团更加真实一点,避免在团结束那一刻突然成团了)应小于自动过期时间
|
||||
\think\Queue::later($fictitious_time, '\addons\shopro\job\GrouponAutoOper@fictitious', [
|
||||
'activity' => $activity,
|
||||
'activity_groupon_id' => $activityGroupon->id
|
||||
], 'shopro');
|
||||
}
|
||||
|
||||
return $activityGroupon;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 增加团成员记录
|
||||
*/
|
||||
protected function addGrouponLog($order, $user, $item, $activityGroupon)
|
||||
{
|
||||
if (!$activityGroupon) {
|
||||
\think\Log::error('groupon-notfund: order_id: ' . $order['id']);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 增加团成员数量
|
||||
$activityGroupon->setInc('current_num', 1);
|
||||
|
||||
// 增加参团记录
|
||||
$activityGrouponLog = new GrouponLog();
|
||||
$activityGrouponLog->user_id = $user['id'];
|
||||
$activityGrouponLog->nickname = $user['nickname'];
|
||||
$activityGrouponLog->avatar = $user['avatar'];
|
||||
$activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
|
||||
$activityGrouponLog->goods_id = $item['goods_id'];
|
||||
$activityGrouponLog->goods_sku_price_id = $item['goods_sku_price_id'];
|
||||
$activityGrouponLog->activity_id = $item['activity_id'];
|
||||
$activityGrouponLog->is_leader = ($activityGroupon['user_id'] == $user['id']) ? 1 : 0;
|
||||
$activityGrouponLog->is_fictitious = 0;
|
||||
$activityGrouponLog->order_id = $order['id'];
|
||||
$activityGrouponLog->save();
|
||||
|
||||
return $activityGrouponLog;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 【此方法即将废除,加入团之后,不删除参团记录】,删除团成员记录(退款:已经真实加入团了,这里扣除)()
|
||||
*/
|
||||
protected function delGrouponLog($order, $groupon)
|
||||
{
|
||||
$activityGrouponLog = GrouponLog::where('user_id', $order->user_id)
|
||||
->where('groupon_id', $groupon->id)
|
||||
->where('order_id', $order->id)
|
||||
->find();
|
||||
|
||||
if ($activityGrouponLog) {
|
||||
$activityGrouponLog->delete();
|
||||
|
||||
// 扣除参团人数
|
||||
$groupon->setDec('current_num', 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 订单退款时标记拼团记录为已退款(主动退款和拼团失败退款)
|
||||
*
|
||||
* @param \think\Model $order
|
||||
* @return void
|
||||
*/
|
||||
protected function refundGrouponLog($order)
|
||||
{
|
||||
$order_ext = $order['ext'];
|
||||
$groupon_id = $order_ext['groupon_id'] ?? 0;
|
||||
if (!$groupon_id) {
|
||||
return true; // 商品独立购买,未参团,或者开新团
|
||||
}
|
||||
|
||||
$activityGrouponLog = GrouponLog::where('user_id', $order->user_id)
|
||||
->where('groupon_id', $groupon_id)
|
||||
->where('order_id', $order->id)
|
||||
->find();
|
||||
|
||||
if ($activityGrouponLog) {
|
||||
// 修改 logs 为已退款
|
||||
$activityGrouponLog->is_refund = 1;
|
||||
$activityGrouponLog->save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 虚拟成团,增加虚拟成员,并判断是否完成,然后将团状态改为,虚拟成团成功
|
||||
protected function finishFictitiousGroupon($activity, $activityGroupon, $invalid = true, $num = 0, $users = [])
|
||||
{
|
||||
// 拼团剩余人数
|
||||
$surplus_num = $activityGroupon['num'] - $activityGroupon['current_num'];
|
||||
|
||||
// 团已经满员
|
||||
if ($surplus_num <= 0) {
|
||||
if ($activityGroupon['status'] == 'ing') {
|
||||
// 已满员但还是进行中状态,检测并完成团,起到纠正作用
|
||||
return $this->checkGrouponStatus($activityGroupon);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 本次虚拟人数, 如果传入 num 则使用 num 和 surplus_num 中最小值, 如果没有传入,默认剩余人数全部虚拟
|
||||
$fictitious_num = $num ? ($num > $surplus_num ? $surplus_num : $num) : $surplus_num;
|
||||
|
||||
$fakeUsers = FakeUser::orderRaw('rand()')->limit($fictitious_num)->select();
|
||||
|
||||
if (count($fakeUsers) < $fictitious_num && $num == 0) {
|
||||
if ($invalid) {
|
||||
// 虚拟用户不足,并且是自动虚拟成团进程,自动解散团
|
||||
return $this->invalidRefundGroupon($activityGroupon);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 增加团人数
|
||||
$activityGroupon->setInc('current_num', $fictitious_num);
|
||||
|
||||
if (has_redis()) {
|
||||
// redis 参数
|
||||
$keys = ActivityRedis::keysActivity([
|
||||
'groupon_id' => $activityGroupon['id'],
|
||||
'goods_id' => $activityGroupon['goods_id'],
|
||||
], [
|
||||
'activity_id' => $activity['id'],
|
||||
'activity_type' => $activity['type'],
|
||||
]);
|
||||
|
||||
extract($keys);
|
||||
|
||||
Redis::HINCRBY($keyActivity, $keyGrouponNum, $fictitious_num); // 增加 redis 参团人数
|
||||
|
||||
// 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消失效订单时删除缓存
|
||||
$userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
|
||||
$userList = json_decode($userList, true);
|
||||
$userList = $userList ?: [];
|
||||
for ($i =0; $i < $fictitious_num; $i++) {
|
||||
$userList[] = [
|
||||
'user_id' => 'fictitiou_' . time() . mt_rand(1000, 9999),
|
||||
];
|
||||
}
|
||||
Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $fictitious_num; $i++) {
|
||||
// 先用传过来的
|
||||
$avatar = isset($users[$i]['avatar']) ? $users[$i]['avatar'] : '';
|
||||
$nickname = isset($users[$i]['nickname']) ? $users[$i]['nickname'] : '';
|
||||
|
||||
// 如果没有,用查的虚拟的
|
||||
$avatar = $avatar ?: $fakeUsers[$i]['avatar'];
|
||||
$nickname = $nickname ?: $fakeUsers[$i]['nickname'];
|
||||
|
||||
// 增加参团记录
|
||||
$activityGrouponLog = new GrouponLog();
|
||||
$activityGrouponLog->user_id = 0;
|
||||
$activityGrouponLog->nickname = $nickname;
|
||||
$activityGrouponLog->avatar = $avatar;
|
||||
$activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
|
||||
$activityGrouponLog->goods_id = $activityGroupon['goods_id'];
|
||||
$activityGrouponLog->goods_sku_price_id = 0; // 没有订单,所以也就没有 goods_sku_price_id
|
||||
$activityGrouponLog->activity_id = $activityGroupon['activity_id'];
|
||||
$activityGrouponLog->is_leader = 0; // 不是团长
|
||||
$activityGrouponLog->is_fictitious = 1; // 虚拟用户
|
||||
$activityGrouponLog->order_id = 0; // 虚拟成员没有订单
|
||||
$activityGrouponLog->save();
|
||||
}
|
||||
|
||||
return $this->checkGrouponStatus($activityGroupon);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 团过期退款,或者后台手动解散退款
|
||||
*/
|
||||
protected function invalidRefundGroupon($activityGroupon, $user = null)
|
||||
{
|
||||
$activityGroupon->status = 'invalid'; // 拼团失败
|
||||
$activityGroupon->save();
|
||||
|
||||
// 查询参团真人
|
||||
$logs = GrouponLog::with(['order'])->where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 0)->select();
|
||||
|
||||
foreach ($logs as $key => $log) {
|
||||
$order = $log->order;
|
||||
if ($order && in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED])) {
|
||||
$refundNum = OrderItem::where('order_id', $order->id)->where('refund_status', '<>', OrderItem::REFUND_STATUS_NOREFUND)->count();
|
||||
if (!$refundNum) {
|
||||
// 无条件全额退款
|
||||
$refund = new OrderRefund($order);
|
||||
$refund->fullRefund($user, [
|
||||
'remark' => '拼团失败退款'
|
||||
]);
|
||||
}
|
||||
} else if ($order && $order->isOffline($order)) {
|
||||
$orderOper = new OrderOper();
|
||||
$orderOper->cancel($order, null, 'system', '拼团失败,系统自动取消订单');
|
||||
}
|
||||
}
|
||||
|
||||
// 触发拼团失败行为
|
||||
$data = ['groupon' => $activityGroupon];
|
||||
\think\Hook::listen('activity_groupon_fail', $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查团状态
|
||||
*/
|
||||
protected function checkGrouponStatus($activityGroupon)
|
||||
{
|
||||
if (!$activityGroupon) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 重新获取团信息
|
||||
$activityGroupon = ActivityGroupon::where('id', $activityGroupon['id'])->find();
|
||||
if ($activityGroupon['current_num'] >= $activityGroupon['num'] && !in_array($activityGroupon['status'], ['finish', 'finish_fictitious'])) {
|
||||
// 查询是否有虚拟团成员
|
||||
$fictitiousCount = GrouponLog::where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 1)->count();
|
||||
|
||||
// 将团设置为已完成
|
||||
$activityGroupon->status = $fictitiousCount ? 'finish_fictitious' : 'finish';
|
||||
$activityGroupon->finish_time = time();
|
||||
$activityGroupon->save();
|
||||
|
||||
// 触发成团行为
|
||||
$data = ['groupon' => $activityGroupon];
|
||||
\think\Hook::listen('activity_groupon_finish', $data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
3347
addons/shopro/library/cacert.pem
Normal file
3347
addons/shopro/library/cacert.pem
Normal file
File diff suppressed because it is too large
Load Diff
324
addons/shopro/library/chat/Chat.php
Normal file
324
addons/shopro/library/chat/Chat.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\DebugEvent;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\NspData;
|
||||
use addons\shopro\library\chat\traits\BindUId;
|
||||
use PHPSocketIO\SocketIO;
|
||||
use PHPSocketIO\Socket;
|
||||
use PHPSocketIO\Nsp;
|
||||
|
||||
class Chat
|
||||
{
|
||||
/**
|
||||
* session 存储助手
|
||||
*/
|
||||
use Session;
|
||||
|
||||
/**
|
||||
* 绑定 UID 助手
|
||||
*/
|
||||
use BindUId;
|
||||
/**
|
||||
* 绑定数据到 nsp 作为全局数据
|
||||
*/
|
||||
use NspData;
|
||||
/**
|
||||
* 助手方法
|
||||
*/
|
||||
use Helper;
|
||||
|
||||
/**
|
||||
* debug 方式注册事件
|
||||
*/
|
||||
use DebugEvent;
|
||||
|
||||
/**
|
||||
* 当前 phpsocket.io 实例
|
||||
*
|
||||
* @var SocketIO
|
||||
*/
|
||||
public $io;
|
||||
|
||||
/**
|
||||
* 当前连接实例
|
||||
*
|
||||
* @var Socket
|
||||
*/
|
||||
public $socket;
|
||||
|
||||
/**
|
||||
* 当前 namespace 实例
|
||||
*
|
||||
* @var Nsp
|
||||
*/
|
||||
public $nsp;
|
||||
|
||||
/**
|
||||
* 当前发送实例
|
||||
*
|
||||
* @var Sender
|
||||
*/
|
||||
public $sender;
|
||||
|
||||
/**
|
||||
* 当前获取数据
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
public $getter;
|
||||
|
||||
/**
|
||||
* chat 操作类
|
||||
*
|
||||
* @var ChatService
|
||||
*/
|
||||
public $chatService;
|
||||
|
||||
protected $auth = [
|
||||
'user',
|
||||
'admin',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* 初始化 chat 系统
|
||||
*
|
||||
* @param SocketIo $io
|
||||
* @param Socket $socket
|
||||
* @param Nsp $nsp
|
||||
*/
|
||||
public function __construct(SocketIo $io, Nsp $nsp, Socket $socket = null)
|
||||
{
|
||||
$this->io = $io;
|
||||
$this->socket = $socket;
|
||||
$this->nsp = $nsp;
|
||||
|
||||
// 初始化获取更改数据实例
|
||||
$this->getter = new Getter($socket, $io, $nsp);
|
||||
// 初始化发送实例
|
||||
$this->sender = new Sender($socket, $io, $nsp, $this->getter);
|
||||
// 初始化 客服公共方法实例
|
||||
$this->chatService = new ChatService($socket, $io, $nsp, $this->getter);
|
||||
}
|
||||
|
||||
public function on()
|
||||
{
|
||||
// on 方法只有在连接的时候走一次
|
||||
$this->register('test', function ($data, $callback) {
|
||||
|
||||
// $class = "\\app\\chat\\library\\provider\\auth\\User";
|
||||
// $provider = new $class($this);
|
||||
|
||||
// $this->socket->removeAllListeners('message');
|
||||
|
||||
// 注册相关身份事件
|
||||
// $provider->customerEvent();
|
||||
|
||||
|
||||
$customer_service_room = $this->getRoomName('customer_service_room', ['room_id' => 'admin']);
|
||||
$customerServices = $this->getter('socket')->getSessionsByRoom($customer_service_room, 'customer_service');
|
||||
|
||||
$this->sender->successSocket($callback, '连接成功', [
|
||||
'msg' => '恭喜鏈接成功',
|
||||
'bind' => $this->nsp->bind ?? [],
|
||||
'nsp_room_ids' => $this->nspData('room_ids'),
|
||||
'customer_service' => $customerServices,
|
||||
'nsp_data' => $this->nsp->nspData ?? [],
|
||||
'rooms' => isset($this->nsp->adapter->rooms) ? $this->nsp->adapter->rooms : [],
|
||||
'current_rooms' => $this->socket->rooms,
|
||||
'session' => $this->session(),
|
||||
'client_id' => $this->socket->id,
|
||||
'session_ids' => $this->nspData('session_ids')
|
||||
]);
|
||||
|
||||
|
||||
$this->sender->successUId('new message', '消息桶送', ['aaa' => 'bbb'], [
|
||||
'id' => $this->session('session_id'),
|
||||
'type' => $this->session('auth'),
|
||||
]);
|
||||
|
||||
// foreach ($clientIds as $client_id) {
|
||||
// $this->sender->successSocket('new message', ['aaa' => 'bbb']);
|
||||
// }
|
||||
|
||||
$this->socket->on('test-child', function ($data, $callback) {
|
||||
echo "子集消息来了";
|
||||
|
||||
$this->session('text:child', 'aaa');
|
||||
$this->sender->successSocket($callback, '连接成功', [
|
||||
'msg' => '子事件夜之星成功了'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// socket 连接初始化,socket.io-client 连接后的第一步
|
||||
$this->register('connection', function ($data, $callback) {
|
||||
// 初始化连接
|
||||
$auth = $data['auth'] ?? '';
|
||||
|
||||
if (!in_array($auth, $this->auth)) {
|
||||
throw new ShoproException('身份错误');
|
||||
}
|
||||
|
||||
// 存储当前 auth 驱动
|
||||
$this->session('auth', $auth);
|
||||
|
||||
// 加入对应身份组
|
||||
$this->socket->join($this->getRoomName('auth', ['auth' => $auth]));
|
||||
|
||||
// 加入在线连接组
|
||||
$this->socket->join('online');
|
||||
|
||||
// 检测并自动登录
|
||||
$result = $this->chatService->authLogin($data);
|
||||
|
||||
// 注册各自身份的事件
|
||||
$this->authEvent($auth);
|
||||
|
||||
// 连接成功,发送给自己
|
||||
$this->sender->authSuccess($callback);
|
||||
});
|
||||
|
||||
// auth 身份登录,管理员或者用户
|
||||
$this->register('login', function ($data, $callback) {
|
||||
// 登录,和系统中的用户或者管理员绑定
|
||||
$result = $this->chatService->authLogin($data);
|
||||
if ($result) {
|
||||
// 登录成功
|
||||
$this->sender->authSuccess($callback);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 登录失败
|
||||
throw new ShoproException('登录失败');
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
$this->register('disconnect', function ($data, $callback) {
|
||||
$customer_service_id = $this->session('customer_service_id');
|
||||
$session_id = $this->session('session_id');
|
||||
$identify = $this->session('identify') ?: '';
|
||||
// $auth = $this->session('auth');
|
||||
// $authUser = $this->session('auth_user');
|
||||
|
||||
// 断开连接,解绑 bind 的身份
|
||||
$this->chatService->disconnectUnbindAll();
|
||||
|
||||
// 如果登录了,并且所有客户端都下线了, 删除相关 auth 的 ids
|
||||
// if ($this->chatService->isLogin() && !$this->getter('socket')->isOnlineAuthBySessionId($session_id, $auth)) {
|
||||
// $this->nspSessionIdDel($session_id, $auth);
|
||||
// }
|
||||
|
||||
// 如果是顾客
|
||||
if ($identify == 'customer') {
|
||||
// 顾客所在房间
|
||||
$room_id = $this->session('room_id');
|
||||
|
||||
// 顾客断开连接
|
||||
if (!$this->getter('socket')->isOnLineCustomerById($session_id)) {
|
||||
// 当前所遇用户端都断开了
|
||||
|
||||
$waiting_room_name = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
|
||||
$rooms = $this->socket->rooms;
|
||||
// 判断是否在排队中
|
||||
if (in_array($waiting_room_name, $rooms)) {
|
||||
// 这里顾客的所有客户端都断开了,在排队排名中移除
|
||||
$this->nspWaitingDel($room_id, $session_id);
|
||||
|
||||
// 排队发生变化,通知房间中所有排队用户
|
||||
$this->sender->allWaitingQueue($room_id);
|
||||
|
||||
// 离开排队中房间(将离线的用户从等待中移除)
|
||||
$this->socket->leave($waiting_room_name);
|
||||
// 通知更新排队中列表,把当前下线用户移除
|
||||
$this->sender->waiting();
|
||||
}
|
||||
|
||||
// 通知所有客服,顾客下线
|
||||
$this->sender->customerOffline();
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是客服
|
||||
if ($identify == 'customer_service') {
|
||||
// 客服断开连接
|
||||
if (!$this->getter('socket')->isOnLineCustomerServiceById($customer_service_id)) {
|
||||
// 当前客服的所有客户端都下线了
|
||||
|
||||
// 更新客服状态为离线
|
||||
$this->getter()->updateCustomerServiceStatus($customer_service_id, 'offline');
|
||||
|
||||
// 通知连接的用户(在当前客服服务的房间里面的用户),客服下线了
|
||||
$this->sender->customerServiceOffline();
|
||||
|
||||
// 通知当前房间的在线客服,更新当前在线客服列表
|
||||
$this->sender->customerServiceUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 注册相关身份事件
|
||||
*
|
||||
* @param string $auth
|
||||
* @return void
|
||||
*/
|
||||
private function authEvent($auth)
|
||||
{
|
||||
// 实例化相关身份事件
|
||||
$class = "\\addons\\shopro\\library\\chat\\provider\\auth\\" . ucfirst($auth);
|
||||
$provider = new $class($this);
|
||||
|
||||
// 注册相关身份事件
|
||||
$provider->on();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 站内信通知
|
||||
*
|
||||
* @param object $http_connection
|
||||
* @param string $uri
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function innerWorker($httpConnection, $uri, $data)
|
||||
{
|
||||
if ($uri == '/notification') {
|
||||
$this->exec($httpConnection, function () use ($data) {
|
||||
$receiver = $data['receiver'] ?? [];
|
||||
$sendData = $data['data'] ?? [];
|
||||
|
||||
$receiver_type = $receiver['type'] ?? 'user';
|
||||
$receiverIds = $receiver['ids'] ?? '';
|
||||
$receiverIds = is_array($receiverIds) ? $receiverIds : explode(',', $receiverIds);
|
||||
|
||||
// 循环给接收者发送消息
|
||||
foreach ($receiverIds as $id) {
|
||||
// 获取接收人的 session_id
|
||||
$session_id = $this->getter()->getSessionIdByAuth($id, $receiver_type);
|
||||
if ($session_id) {
|
||||
$this->sender->successUId('notification', '收到通知', $sendData, [
|
||||
'id' => $session_id, 'type' => $receiver_type
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 这句话必须有,否则会提示超时
|
||||
$httpConnection->send('ok');
|
||||
}
|
||||
}
|
||||
856
addons/shopro/library/chat/ChatService.php
Normal file
856
addons/shopro/library/chat/ChatService.php
Normal file
@@ -0,0 +1,856 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\NspData;
|
||||
use addons\shopro\library\chat\traits\BindUId;
|
||||
use app\admin\model\shopro\chat\User as ChatUser;
|
||||
use app\admin\model\shopro\Config;
|
||||
use PHPSocketIO\SocketIO;
|
||||
use PHPSocketIO\Socket;
|
||||
use PHPSocketIO\Nsp;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
* ChatService 服务类
|
||||
*/
|
||||
class ChatService
|
||||
{
|
||||
|
||||
/**
|
||||
* session 存储助手
|
||||
*/
|
||||
use Session;
|
||||
|
||||
/**
|
||||
* 绑定 UID 助手
|
||||
*/
|
||||
use BindUId;
|
||||
/**
|
||||
* nsp 对象存储全局数据
|
||||
*/
|
||||
use NspData;
|
||||
|
||||
/**
|
||||
* 助手方法
|
||||
*/
|
||||
use Helper;
|
||||
|
||||
/**
|
||||
* 当前 phpsocket.io 实例
|
||||
*
|
||||
* @var SocketIO
|
||||
*/
|
||||
protected $io;
|
||||
/**
|
||||
* 当前socket 连接
|
||||
*
|
||||
* @var Socket
|
||||
*/
|
||||
protected $socket;
|
||||
/**
|
||||
* 当前 命名空间
|
||||
*
|
||||
* @var Nsp
|
||||
*/
|
||||
public $nsp;
|
||||
|
||||
/**
|
||||
* getter 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $getter;
|
||||
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*
|
||||
* @param Socket $socket
|
||||
* @param SocketIO $io
|
||||
* @param Nsp $nsp
|
||||
* @param Getter $getter
|
||||
*/
|
||||
public function __construct(Socket $socket = null, SocketIO $io, Nsp $nsp, Getter $getter)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->io = $io;
|
||||
$this->nsp = $nsp;
|
||||
|
||||
$this->getter = $getter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 client_id 将当前 client_id,加入对应 room
|
||||
*
|
||||
* @param string $client_id
|
||||
* @param string $room
|
||||
* @return boolean
|
||||
*/
|
||||
public function joinByClientId($client_id, $room)
|
||||
{
|
||||
// 找到 client_id 对应的 socket 实例
|
||||
$client = $this->nsp->sockets[$client_id];
|
||||
|
||||
$client->join($room);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 session_id 将 session_id 对应的所有客户端加入 room
|
||||
*
|
||||
* @param string $room
|
||||
* @param string $session_id
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function joinBySessionId($session_id, $type, $room)
|
||||
{
|
||||
// 当前用户 uid 绑定的所有客户端 clientIds
|
||||
$clientIds = $this->getClientIdByUId($session_id, $type);
|
||||
|
||||
// 将当前 session_id 绑定的 client_id 都加入当前客服组
|
||||
foreach ($clientIds as $client_id) {
|
||||
$this->joinByClientId($client_id, $room);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 client_id 将当前 client_id,离开对应 room
|
||||
*
|
||||
* @param string $client_id
|
||||
* @param string $room
|
||||
* @return boolean
|
||||
*/
|
||||
public function leaveByClientId($client_id, $room)
|
||||
{
|
||||
// 找到 client_id 对应的 socket 实例
|
||||
$client = $this->nsp->sockets[$client_id];
|
||||
|
||||
$client->leave($room);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断 auth 是否登录
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLogin()
|
||||
{
|
||||
$user = $this->session('auth_user');
|
||||
return $user ? true : false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* auth 登录
|
||||
*
|
||||
* @param array $data 登录参数,token session_id 等
|
||||
* @return boolean
|
||||
*/
|
||||
public function authLogin($data)
|
||||
{
|
||||
if ($this->isLogin()) {
|
||||
return true;
|
||||
}
|
||||
$auth = $this->session('auth');
|
||||
$user = null;
|
||||
$token = $data['token'] ?? ''; // fastadmin token
|
||||
$session_id = $data['session_id'] ?? ''; // session_id 如果没有,则后端生成
|
||||
$session_id = $session_id ?: $this->session('session_id'); // 如果没有,默认取缓存中的
|
||||
|
||||
// 根据 token 获取当前登录的用户
|
||||
if ($token) {
|
||||
$user = $this->getter('db')->getAuthByToken($token, $auth);
|
||||
}
|
||||
|
||||
if (!$user && $session_id) {
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
$auth_id = $chatUser ? $chatUser['auth_id'] : 0;
|
||||
$user = $this->getter('db')->getAuthById($auth_id, $auth);
|
||||
}
|
||||
|
||||
// 初始化连接,需要获取 session_id
|
||||
if (!$session_id) {
|
||||
// 如果没有 session_id
|
||||
if ($user) {
|
||||
// 如果存在 user
|
||||
$chatUser = $this->getter('db')->getChatUserByAuth($user['id'], $auth);
|
||||
$session_id = $chatUser ? $chatUser['session_id'] : '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$session_id) {
|
||||
// 如果依然没有 session_id, 随机生成 session_id
|
||||
$session_id = md5(time() . mt_rand(1000000, 9999999));
|
||||
}
|
||||
|
||||
// 更新顾客用户信息
|
||||
$chatUser = $this->updateChatUser($session_id, $user, $auth);
|
||||
$this->session('chat_user', $chatUser->toArray()); // 转为数组
|
||||
$this->session('session_id', $session_id);
|
||||
$this->session('auth_user', $user ? $user->toArray() : $user);
|
||||
|
||||
// bind auth标示session_id,绑定 client_id
|
||||
$this->bindUId($this->session('session_id'), $auth);
|
||||
|
||||
if ($user) {
|
||||
$this->loginOk();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* auth 登录成功,注册相关事件
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loginOk()
|
||||
{
|
||||
$auth = $this->session('auth');
|
||||
$session_id = $this->session('session_id');
|
||||
|
||||
// 将登陆的 auth 记录下来
|
||||
// $this->nspSessionIdAdd($session_id, $auth);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新 chat_user
|
||||
*
|
||||
* @param string $session_id
|
||||
* @param array $auth
|
||||
* @param string $auth
|
||||
* @return array|object
|
||||
*/
|
||||
public function updateChatUser($session_id, $authUser, $auth)
|
||||
{
|
||||
// 这里只根据 session_id 查询,
|
||||
// 当前 session_id 如果已经绑定了 auth 和 auth_id,会被 authUser 覆盖掉,无论是否是同一个 auth 和 auth_id
|
||||
// 不在 根据 authUser 或者 session_id 同时查
|
||||
// 用户匿名使用客服,登录绑定用户之后,又退出登录其他用户,那么这个 session_id 和老的聊天记录直接归新用户所有
|
||||
$chatUser = ChatUser::where('auth', $auth)->where(function ($query) use ($session_id, $authUser) {
|
||||
$query->where('session_id', $session_id);
|
||||
// ->whereOr(function ($query) use ($authUser) {
|
||||
// $query->where('auth_id', '<>', 0)
|
||||
// ->where('auth_id', ($authUser ? $authUser['id'] : 0));
|
||||
// });
|
||||
})->find();
|
||||
|
||||
$defaultUser = Config::getConfigs('basic.user');
|
||||
$defaultAvatar = $defaultUser['avatar'] ?? null;
|
||||
// $defaultNickname = $defaultUser['nickname'] ?? null;
|
||||
|
||||
if (!$chatUser) {
|
||||
$chatUser = new ChatUser();
|
||||
|
||||
$chatUser->session_id = $session_id;
|
||||
$chatUser->auth = $auth;
|
||||
$chatUser->auth_id = $authUser ? $authUser['id'] : 0;
|
||||
$chatUser->nickname = $authUser ? $authUser['nickname'] : ('游客-' . substr($session_id, 0, 5));
|
||||
$chatUser->avatar = $authUser ? $authUser->getData('avatar') : $defaultAvatar;
|
||||
$chatUser->customer_service_id = 0; // 断开连接的时候存入
|
||||
$chatUser->last_time = time();
|
||||
} else {
|
||||
if ($authUser) {
|
||||
// 更新用户信息
|
||||
$chatUser->auth = $auth;
|
||||
$chatUser->auth_id = $authUser['id'] ?? 0;
|
||||
$chatUser->nickname = $authUser['nickname'] ? $authUser['nickname'] : ('游客-' . substr($session_id, 0, 5));
|
||||
$chatUser->avatar = $authUser['avatar'] ? $authUser->getData('avatar') : $defaultAvatar;
|
||||
}
|
||||
|
||||
$chatUser->last_time = time(); // 更新时间
|
||||
}
|
||||
|
||||
$chatUser->save();
|
||||
return $chatUser;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检测并且分配客服
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function checkAndAllocatCustomerService()
|
||||
{
|
||||
$room_id = $this->session('room_id');
|
||||
$session_id = $this->session('session_id');
|
||||
$auth = $this->session('auth');
|
||||
|
||||
$customerService = null;
|
||||
// 获取用户临时存的 客服
|
||||
$customer_service_id = $this->nspGetConnectionCustomerServiceId($room_id, $session_id);
|
||||
if ($customer_service_id) {
|
||||
$customerService = $this->getter('socket')->getCustomerServiceById($room_id, $customer_service_id);
|
||||
}
|
||||
|
||||
if (!$customerService) {
|
||||
// 获取当前顾客有没有其他端正在连接客服,如果有,直接获取该客服
|
||||
$customerService = $this->getter('socket')->getCustomerServiceBySessionId($room_id, $session_id);
|
||||
}
|
||||
|
||||
if (!$customerService) {
|
||||
$chatBasic = $this->getConfig('basic');
|
||||
if ($chatBasic['auto_customer_service']) {
|
||||
// 自动分配客服
|
||||
$chatUser = $this->session('chat_user');
|
||||
$customerService = $this->allocatCustomerService($room_id, $chatUser);
|
||||
}
|
||||
}
|
||||
|
||||
// 分配了客服
|
||||
if ($customerService) {
|
||||
// 将当前 用户与 客服绑定
|
||||
$this->bindCustomerServiceBySessionId($room_id, $session_id, $customerService);
|
||||
} else {
|
||||
// 加入等待组中
|
||||
$room = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
|
||||
$this->joinBySessionId($session_id, $auth, $room);
|
||||
|
||||
// 将用户 session_id 加入到等待排名中,自动排重
|
||||
$this->nspWaitingAdd($room_id, $session_id);
|
||||
}
|
||||
|
||||
return $customerService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 分配客服
|
||||
*
|
||||
* @param string $room_id, 客服房间号
|
||||
* @return array
|
||||
*/
|
||||
private function allocatCustomerService($room_id, $chatUser)
|
||||
{
|
||||
$config = $this->getConfig('basic');
|
||||
|
||||
$last_customer_service = $config['last_customer_service'] ?? 1;
|
||||
$allocate = $config['allocate'] ?? 'busy';
|
||||
|
||||
// 分配的客服
|
||||
$currentCustomerService = null;
|
||||
|
||||
// 使用上次客服
|
||||
if ($last_customer_service) {
|
||||
// 获取上次连接的信息
|
||||
$lastServiceLog = $this->getter('db')->getLastServiceLogByChatUser($room_id, $chatUser['id']);
|
||||
|
||||
if ($lastServiceLog) {
|
||||
// 获取上次客服信息
|
||||
$currentCustomerService = $this->getter('socket')->getCustomerServiceById($room_id, $lastServiceLog['customer_service_id']); // 通过连接房间,获取socket 连接里面上次客服,不在线为 null
|
||||
}
|
||||
}
|
||||
|
||||
// 没有客服,随机分配
|
||||
if (!$currentCustomerService) {
|
||||
// 在线客服列表
|
||||
$onlineCustomerServices = $this->getter('socket')->getCustomerServicesByRoomId($room_id);
|
||||
|
||||
if ($onlineCustomerServices) {
|
||||
if ($allocate == 'busy') {
|
||||
// 将客服列表,按照工作繁忙程度正序排序, 这里没有离线的客服
|
||||
$onlineCustomerServices = array_column($onlineCustomerServices, null, 'busy_percent');
|
||||
ksort($onlineCustomerServices);
|
||||
|
||||
// 取忙碌度最小,并且客服为 正常在线状态
|
||||
foreach ($onlineCustomerServices as $customerService) {
|
||||
if ($customerService['status'] == 'online') {
|
||||
$currentCustomerService = $customerService;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ($allocate == 'turns') {
|
||||
// 按照最后接入时间正序排序,这里没有离线的客服
|
||||
$onlineCustomerServices = array_column($onlineCustomerServices, null, 'last_time');
|
||||
ksort($onlineCustomerServices);
|
||||
|
||||
// 取最后接待最早,并且客服为 正常在线状态
|
||||
foreach ($onlineCustomerServices as $customerService) {
|
||||
if ($customerService['status'] == 'online') {
|
||||
$currentCustomerService = $customerService;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ($allocate == 'random') {
|
||||
// 随机获取一名客服
|
||||
|
||||
// 挑出来状态为在线的客服
|
||||
$onlineStatus = [];
|
||||
foreach ($onlineCustomerServices as $customerService) {
|
||||
if ($customerService['status'] == 'online') {
|
||||
$onlineStatus[] = $customerService;
|
||||
}
|
||||
}
|
||||
|
||||
$onlineStatus = array_column($onlineStatus, null, 'id');
|
||||
|
||||
$customer_service_id = 0;
|
||||
if ($onlineStatus) {
|
||||
$customer_service_id = array_rand($onlineStatus);
|
||||
}
|
||||
|
||||
$currentCustomerService = $onlineStatus[$customer_service_id] ?? null;
|
||||
}
|
||||
|
||||
if (!$currentCustomerService) {
|
||||
// 如果都不是 online 状态(说明全是 busy),默认取第一条
|
||||
$currentCustomerService = $onlineCustomerServices[0] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $currentCustomerService;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 session_id 记录客服信息,并且加入对应的客服组
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @param string $session_id 用户
|
||||
* @param array $customerService 客服信息
|
||||
* @return void
|
||||
*/
|
||||
public function bindCustomerServiceBySessionId($room_id, $session_id, $customerService)
|
||||
{
|
||||
// 当前用户 uid 绑定的所有客户端 clientIds
|
||||
$clientIds = $this->getClientIdByUId($session_id, 'customer');
|
||||
|
||||
// 将当前 session_id 绑定的 client_id 都加入当前客服组
|
||||
foreach ($clientIds as $client_id) {
|
||||
self::bindCustomerServiceByClientId($room_id, $client_id, $customerService);
|
||||
}
|
||||
|
||||
// 添加 serviceLog
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
$this->getter('db')->createServiceLog($room_id, $chatUser, $customerService);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 顾客session 记录客服信息,并且加入对应的客服组
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @param string $client_id 用户
|
||||
* @param array $customerService 客服信息
|
||||
* @return null
|
||||
*/
|
||||
public function bindCustomerServiceByClientId($room_id, $client_id, $customerService)
|
||||
{
|
||||
// 更新用户的客服信息
|
||||
$this->updateSessionByClientId($client_id, [
|
||||
'customer_service_id' => $customerService['id'],
|
||||
'customer_service' => $customerService
|
||||
]);
|
||||
|
||||
// 更新客服的最后接入用户时间
|
||||
$this->getter()->updateCustomerServiceInfo($customerService['id'], ['last_time' => time()]);
|
||||
|
||||
// 加入对应客服组,统计客服信息,通知用户客服上线等
|
||||
$room = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]);
|
||||
$this->joinByClientId($client_id, $room);
|
||||
|
||||
// 从等待接入组移除
|
||||
$room = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
|
||||
$this->leaveByClientId($client_id, $room);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 session_id 将用户移除客服组
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $session_id 用户
|
||||
* @param array $customerService 客服信息
|
||||
* @return null
|
||||
*/
|
||||
public function unBindCustomerServiceBySessionId($room_id, $session_id, $customerService)
|
||||
{
|
||||
// 当前用户 uid 绑定的所有客户端 clientIds
|
||||
$clientIds = $this->getClientIdByUId($session_id, 'customer');
|
||||
|
||||
// 将当前 session_id 绑定的 client_id 都加入当前客服组
|
||||
foreach ($clientIds as $client_id) {
|
||||
$this->unBindCustomerService($room_id, $client_id, $customerService);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将用户的客服信息移除
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $client_id 用户
|
||||
* @param array $customerService 客服信息
|
||||
* @return null
|
||||
*/
|
||||
public function unBindCustomerService($room_id, $client_id, $customerService)
|
||||
{
|
||||
// 清空连接的客服的 session
|
||||
$this->updateSessionByClientId($client_id, [
|
||||
'customer_service_id' => 0,
|
||||
'customer_service' => []
|
||||
]);
|
||||
|
||||
// 移除对应客服组
|
||||
$room = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]);
|
||||
$this->leaveByClientId($client_id, $room);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 客服转接,移除旧的客服,加入新的客服
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @param string $session_id 用户
|
||||
* @param array $customerService 客服信息
|
||||
* @param array $newCustomerService 新客服信息
|
||||
* @return void
|
||||
*/
|
||||
public function transferCustomerServiceBySessionId($room_id, $session_id, $customerService, $newCustomerService)
|
||||
{
|
||||
// 获取session_id 的 chatUser
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
// 结束 serviceLog,如果没有,会创建一条记录
|
||||
$this->endService($room_id, $chatUser, $customerService);
|
||||
|
||||
// 解绑老客服
|
||||
$this->unBindCustomerServiceBySessionId($room_id, $session_id, $customerService);
|
||||
|
||||
// 接入新客服
|
||||
$this->bindCustomerServiceBySessionId($room_id, $session_id, $newCustomerService);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 结束服务
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @param object $chatUser 顾客信息
|
||||
* @param array $customerService 客服信息
|
||||
* @return void
|
||||
*/
|
||||
public function endService($room_id, $chatUser, $customerService)
|
||||
{
|
||||
// 结束掉服务记录
|
||||
$this->getter('db')->endServiceLog($room_id, $chatUser, $customerService);
|
||||
|
||||
// 记录客户最后服务的客服
|
||||
$chatUser->customer_service_id = $customerService['id'] ?? 0;
|
||||
$chatUser->save();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 断开用户的服务
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $session_id
|
||||
* @param array $customerService
|
||||
* @return void
|
||||
*/
|
||||
public function breakCustomerServiceBySessionId($room_id, $session_id, $customerService)
|
||||
{
|
||||
// 获取session_id 的 chatUser
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
if ($chatUser) {
|
||||
// 结束 serviceLog,如果没有,会创建一条记录
|
||||
$this->endService($room_id, $chatUser, $customerService);
|
||||
}
|
||||
|
||||
// 解绑老客服
|
||||
$this->unBindCustomerServiceBySessionId($room_id, $session_id, $customerService);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查当前用户的客服身份
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $auth 用户类型
|
||||
* @param integer $auth_id 用户 id
|
||||
* @return boolean|object
|
||||
*/
|
||||
public function getCustomerServicesByAuth($auth_id, $auth)
|
||||
{
|
||||
$customerServices = $this->getter('db')->getCustomerServicesByAuth($auth_id, $auth);
|
||||
|
||||
return $customerServices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查当前用户在当前房间是否是客服身份
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $auth 用户类型
|
||||
* @param integer $auth_id 用户 id
|
||||
* @return boolean|object
|
||||
*/
|
||||
public function checkIsCustomerService($room_id, $auth_id, $auth)
|
||||
{
|
||||
$currentCustomerService = $this->getter('db')->getCustomerServiceByAuthAndRoom($room_id, $auth_id, $auth);
|
||||
|
||||
return $currentCustomerService ? : false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服登录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $auth_id
|
||||
* @param string $auth
|
||||
* @return boolean
|
||||
*/
|
||||
public function customerServiceLogin($room_id, $auth_id, $auth) : bool
|
||||
{
|
||||
if ($customerService = $this->checkIsCustomerService($room_id, $auth_id, $auth)) {
|
||||
// 保存 客服信息
|
||||
$this->session('customer_service_id', $customerService['id']);
|
||||
$this->session('customer_service', $customerService->toArray()); // toArray 减少内存占用
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服上线
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $customer_service_id
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceOnline($room_id, $customer_service_id)
|
||||
{
|
||||
// 当前 socket 绑定 客服 id
|
||||
$this->bindUId($customer_service_id, 'customer_service');
|
||||
|
||||
// 更新客服状态
|
||||
$this->getter()->updateCustomerServiceStatus($customer_service_id, 'online');
|
||||
|
||||
// 只把当前连接加入在线客服组,作为服务对象,多个连接同时登录一个客服,状态相互隔离
|
||||
$this->socket->join($this->getRoomName('identify', ['identify' => 'customer_service']));
|
||||
|
||||
// 只把当前连接加入当前频道的客服组,为后续多商户做准备
|
||||
$this->socket->join($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
|
||||
// 保存当前客服身份
|
||||
$this->session('identify', 'customer_service');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服离线
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $customer_service_id
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceOffline($room_id, $customer_service_id)
|
||||
{
|
||||
// 只把当前连接移除在线客服组,多个连接同时登录一个客服,状态相互隔离
|
||||
$this->socket->leave($this->getRoomName('identify', ['identify' => 'customer_service']));
|
||||
|
||||
// 只把当前连接移除当前频道的客服组,为后续多商户做准备
|
||||
$this->socket->leave($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
|
||||
// 当前 socket 解绑 客服 id
|
||||
$this->unBindUId($customer_service_id, 'customer_service');
|
||||
|
||||
// 更新客服状态为离线
|
||||
$this->getter()->updateCustomerServiceStatus($customer_service_id, 'offline');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服忙碌
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $customer_service_id
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceBusy($room_id, $customer_service_id)
|
||||
{
|
||||
// 当前 socket 绑定 客服 id
|
||||
$this->bindUId($customer_service_id, 'customer_service');
|
||||
|
||||
// (尝试重新加入,避免用户是从离线切换过来的)只把当前连接加入在线客服组,作为服务对象,多个连接同时登录一个客服,状态相互隔离
|
||||
$this->socket->join($this->getRoomName('identify', ['identify' => 'customer_service']));
|
||||
|
||||
// (尝试重新加入,避免用户是从离线切换过来的)只把当前连接加入当前频道的客服组,为后续多商户做准备
|
||||
$this->socket->join($this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
|
||||
// 更新客服状态为忙碌
|
||||
$this->getter()->updateCustomerServiceStatus($customer_service_id, 'busy');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服退出
|
||||
*
|
||||
* @param [type] $room_id
|
||||
* @param [type] $customer_service_id
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceLogout($room_id, $customer_service_id)
|
||||
{
|
||||
// 退出房间
|
||||
$this->session('room_id', null);
|
||||
|
||||
// 移除当前客服身份
|
||||
$this->session('identify', null);
|
||||
|
||||
// 移除客服信息
|
||||
$this->session('customer_service_id', null);
|
||||
$this->session('customer_service', null); // toArray 减少内存占用
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 顾客上线
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerOnline()
|
||||
{
|
||||
// 用户信息
|
||||
$session_id = $this->session('session_id');
|
||||
$auth = $this->session('auth');
|
||||
|
||||
// 当前 socket 绑定 顾客 id
|
||||
$this->bindUId($session_id, 'customer');
|
||||
// 加入在线顾客组,作为被服务对象
|
||||
$this->socket->join($this->getRoomName('identify', ['identify' => 'customer']));
|
||||
// 保存当前顾客身份
|
||||
$this->session('identify', 'customer');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 顾客下线
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerOffline()
|
||||
{
|
||||
// 用户信息
|
||||
$session_id = $this->session('session_id');
|
||||
$customer_service_id = $this->session('customer_service_id');
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
$chatUser = $this->session('chat_user');
|
||||
|
||||
// 退出房间
|
||||
$this->session('room_id', null);
|
||||
|
||||
// 当前 socket 绑定 顾客 id
|
||||
$this->unbindUId($session_id, 'customer');
|
||||
|
||||
// 离开在线顾客组,作为被服务对象
|
||||
$this->socket->leave($this->getRoomName('identify', ['identify' => 'customer']));
|
||||
// 移除当前顾客身份
|
||||
$this->session('identify', null);
|
||||
|
||||
// 如果有客服正在服务,移除
|
||||
if ($customer_service_id) {
|
||||
// 离开所在客服的服务对象组
|
||||
$this->socket->leave($this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customer_service_id]));
|
||||
|
||||
// 移除客服信息
|
||||
$this->session('customer_service_id', null);
|
||||
$this->session('customer_service', null);
|
||||
|
||||
// 获取session_id 的 chatUser
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
|
||||
if ($this->getter('socket')->isOnLineCustomerById($session_id)) {
|
||||
// 结束 serviceLog,如果没有,会创建一条记录
|
||||
$this->endService($room_id, $chatUser, $customerService);
|
||||
}
|
||||
} else {
|
||||
// 离开等待组
|
||||
$this->socket->leave($this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 断开连接解绑
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disconnectUnbindAll()
|
||||
{
|
||||
// 因为 session 是存在 socket 上的,服务端重启断开连接,或者刷新浏览器 socket 会重新连接,所以存的 session 会全部清空
|
||||
// 服务端重启 会导致 bind 到 io 实例的 bind 的数据丢失,但是如果是前端用户刷新浏览器,discount 事件中就必须要解绑 bind 数据
|
||||
|
||||
$room_id = $this->session('room_id');
|
||||
$session_id = $this->session('session_id');
|
||||
$auth = $this->session('auth');
|
||||
$identify = $this->session('identify') ? : '';
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 解绑顾客身份
|
||||
if ($identify == 'customer') {
|
||||
$this->unbindUId($session_id, 'customer');
|
||||
|
||||
if ($customerService) {
|
||||
// 连接着客服,将客服信息暂存 nsp 中,防止刷新重新连接
|
||||
$this->nspConnectionAdd($room_id, $session_id, $customerService['id']);
|
||||
|
||||
// 如果有客服,定时判断,如果客服掉线了,关闭
|
||||
Timer::add(10, function () use ($room_id, $session_id, $customerService) {
|
||||
// 十秒之后顾客不在线,说明是真的下线了
|
||||
if (!$this->getter('socket')->isOnLineCustomerById($session_id)) {
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
$this->endService($room_id, $chatUser, $customerService);
|
||||
}
|
||||
}, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 解绑客服身份
|
||||
if ($identify == 'customer_service') {
|
||||
$customer_service_id = $this->session('customer_service_id');
|
||||
$this->unbindUId($customer_service_id, 'customer_service');
|
||||
}
|
||||
|
||||
// 将当前的 用户与 client 解绑
|
||||
$this->unbindUId($session_id, $auth);
|
||||
}
|
||||
}
|
||||
301
addons/shopro/library/chat/Getter.php
Normal file
301
addons/shopro/library/chat/Getter.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\NspData;
|
||||
use addons\shopro\library\chat\provider\getter\Db;
|
||||
use addons\shopro\library\chat\provider\getter\Socket;
|
||||
use PHPSocketIO\SocketIO;
|
||||
use PHPSocketIO\Socket as PhpSocket;
|
||||
use PHPSocketIO\Nsp;
|
||||
|
||||
/**
|
||||
* 客服 getter 获取各种用户类
|
||||
*/
|
||||
class Getter
|
||||
{
|
||||
|
||||
/**
|
||||
* session 存储助手
|
||||
*/
|
||||
use Session;
|
||||
/**
|
||||
* helper 助手
|
||||
*/
|
||||
use Helper;
|
||||
/**
|
||||
* 在 nsp 上存储数据
|
||||
*/
|
||||
use NspData;
|
||||
|
||||
/**
|
||||
* 当前 phpsocket.io 实例
|
||||
*
|
||||
* @var SocketIo
|
||||
*/
|
||||
protected $io = null;
|
||||
/**
|
||||
* 当前socket 连接
|
||||
*
|
||||
* @var PhpSocket
|
||||
*/
|
||||
protected $socket = null;
|
||||
/**
|
||||
* 当前 命名空间实例
|
||||
*
|
||||
* @var Nsp
|
||||
*/
|
||||
protected $nsp = null;
|
||||
|
||||
/**
|
||||
* 数据驱动
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $driver = null;
|
||||
|
||||
protected $dbProvider;
|
||||
protected $socketProvider;
|
||||
|
||||
public function __construct(PhpSocket $socket = null, SocketIo $io, Nsp $nsp)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->io = $io;
|
||||
$this->nsp = $nsp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置 getter 驱动
|
||||
*
|
||||
* @param string $driver
|
||||
* @return self
|
||||
*/
|
||||
public function driver($driver) {
|
||||
$this->driver = $driver;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过auth id 获取 session_id
|
||||
*
|
||||
* @param string $auth_id
|
||||
* @param string $auth
|
||||
* @return string|null
|
||||
*/
|
||||
public function getSessionIdByAuth($auth_id, $auth)
|
||||
{
|
||||
$chatUser = $this->driver('db')->getChatUserByAuth($auth_id, $auth);
|
||||
return $chatUser->session_id ?? null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新客服信息,数据库和session 都更新
|
||||
*
|
||||
* @param integer $id 要更新的 客服id
|
||||
* @param array $data 要更新的数据
|
||||
* @return void
|
||||
*/
|
||||
public function updateCustomerServiceInfo($id, $data)
|
||||
{
|
||||
// 更新当前客服的所有连接,比如 last_time
|
||||
$this->driver('socket')->updateSessionsById($id, 'customer_service', ['customer_service' => $data]);
|
||||
$this->driver('db')->updateCustomerService($id, $data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新客服信息,数据库和session 都更新
|
||||
*
|
||||
* @param integer $id 要更新的 客服id
|
||||
* @param string $status 状态 online|offline|busy
|
||||
* @return void
|
||||
*/
|
||||
public function updateCustomerServiceStatus($id, $status)
|
||||
{
|
||||
$data = ['status' => $status];
|
||||
// 只更新当前连接,别的当前客服的连接不更新,比如 status
|
||||
$this->updateSessionByClientId($this->socket->id, ['customer_service' => $data]);
|
||||
|
||||
// 不是 (改为离线,并且还有当前客服的其他链接在线),则修改数据库状态
|
||||
if (!($status == 'offline' && $this->driver('socket')->isOnLineCustomerServiceById($id))) {
|
||||
$this->driver('db')->updateCustomerService($id, $data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新客服忙碌杜
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateCustomerServiceBusyPercent()
|
||||
{
|
||||
// 所有客服的房间
|
||||
$roomIds = $this->nspData('room_ids');
|
||||
$roomIds = $roomIds ? : [];
|
||||
|
||||
foreach ($roomIds as $room_id) {
|
||||
// 当前房间在线客服组的所有客户端
|
||||
$customer_service_room = $this->getRoomName('customer_service_room', ['room_id' => $room_id]);
|
||||
$clientIds = $this->driver('socket')->getClientIdsByRoom($customer_service_room);
|
||||
|
||||
foreach ($clientIds as $client_id) {
|
||||
$customerService = $this->getSession($client_id, 'customer_service');
|
||||
|
||||
if ($customerService) {
|
||||
// 客服服务的对应用户的房间名
|
||||
$customer_service_room_user = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]);
|
||||
$customerService['current_num'] = count($this->driver('socket')->getSessionsByRoom($customer_service_room_user, 'session_id')); // 真实用户数量,不是客户端数量
|
||||
$max_num = $customerService['max_num'] > 0 ? $customerService['max_num'] : 1; // 避免除数为 0
|
||||
$customerService['busy_percent'] = $customerService['current_num'] / $max_num;
|
||||
|
||||
// 只更新当前连接,别的当前客服的连接不更新,比如 status
|
||||
$this->updateSessionByClientId($client_id, ['customer_service' => $customerService]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取客服服务中的用户
|
||||
*
|
||||
* @param integer $room_id 房间号
|
||||
* @param integer $customer_service_id 客服 id
|
||||
* @param array $exceptIds 要排除的ids (正在服务的用户)
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomersIngFormatByCustomerService($room_id, $customer_service_id)
|
||||
{
|
||||
// 获取数据库历史
|
||||
$ings = $this->driver('socket')->getCustomersIngByCustomerService($room_id, $customer_service_id);
|
||||
|
||||
// 格式化数据
|
||||
$ings = $this->chatUsersFormat($room_id, $ings);
|
||||
|
||||
return $ings;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取等待中的用户
|
||||
*
|
||||
* @param integer $room_id 房间号
|
||||
* @param integer $customer_service_id 客服 id
|
||||
* @param array $exceptIds 要排除的ids (正在服务的用户)
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomersFormatWaiting($room_id)
|
||||
{
|
||||
// 获取数据库历史
|
||||
$waitings = $this->driver('socket')->getCustomersWaiting($room_id);
|
||||
|
||||
// 格式化数据
|
||||
$waitings = $this->chatUsersFormat($room_id, $waitings);
|
||||
|
||||
return $waitings;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取客服服务的历史用户
|
||||
*
|
||||
* @param integer $room_id 房间号
|
||||
* @param integer $customer_service_id 客服 id
|
||||
* @param array $exceptIds 要排除的ids (正在服务的用户)
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomersHistoryFormatByCustomerService($room_id, $customer_service_id, $exceptIds = [])
|
||||
{
|
||||
// 获取数据库历史
|
||||
$histories = $this->driver('db')->getCustomersHistoryByCustomerService($room_id, $customer_service_id, $exceptIds = []);
|
||||
|
||||
// 格式化数据
|
||||
$histories = $this->chatUsersFormat($room_id, $histories, ['select_identify' => 'customer', 'is_customer_service' => true]);
|
||||
|
||||
return $histories;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置最后一条消息和未读消息数
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param array $chatUsers
|
||||
* @param string $select_identify 查询对象,默认客服查用户的,用户查客服的
|
||||
* @return array
|
||||
*/
|
||||
public function chatUsersFormat($room_id, $chatUsers, $params = [])
|
||||
{
|
||||
$select_identify = $params['select_identify'] ?? 'customer';
|
||||
$is_customer_service = $params['is_customer_service'] ?? false;
|
||||
$first = $params['first'] ?? false;
|
||||
|
||||
foreach ($chatUsers as &$chatUser) {
|
||||
// 获取在线状态
|
||||
$status = $this->driver('socket')->isUIdOnline($chatUser['session_id'], 'customer');
|
||||
$chatUser['status'] = $status; // 在线状态
|
||||
|
||||
// 最后一条消息,未读消息条数
|
||||
$chatUser['last_message'] = $this->driver('db')->getMessageLastByChatUser($room_id, $chatUser['id']);
|
||||
$chatUser['unread_num'] = $this->driver('db')->getMessageUnReadNumByChatUserAndIndentify($room_id, $chatUser['id'], $select_identify);
|
||||
|
||||
// 当前的客服
|
||||
$chatUser['customer_service'] = null;
|
||||
if ($is_customer_service) { // 需要客户的客服信息,【历史用户中】
|
||||
$chatUser['customer_service'] = $this->driver('socket')->getCustomerServiceByCustomerSessionId($chatUser['session_id']); // 如果在线,并且已经接入客服,当前客服信息
|
||||
}
|
||||
}
|
||||
|
||||
return $first ? current($chatUsers) : $chatUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* auth 实例方法注入
|
||||
*
|
||||
* @return Db|Socket
|
||||
*/
|
||||
public function provider()
|
||||
{
|
||||
$classProt = $this->driver . 'Provider';
|
||||
|
||||
if ($this->$classProt) {
|
||||
return $this->$classProt;
|
||||
}
|
||||
|
||||
$class = "\\addons\\shopro\\library\\chat\\provider\\getter\\" . ucfirst($this->driver);
|
||||
|
||||
if (class_exists($class)) {
|
||||
$this->$classProt = new $class($this, $this->socket, $this->io, $this->nsp);
|
||||
|
||||
return $this->$classProt;
|
||||
}
|
||||
|
||||
throw new ShoproException('getter 驱动不支持');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 静态调用
|
||||
*
|
||||
* @param string $funcname
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
return $this->provider()->{$funcname}(...$arguments);
|
||||
}
|
||||
}
|
||||
259
addons/shopro/library/chat/Sender.php
Normal file
259
addons/shopro/library/chat/Sender.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat;
|
||||
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\BindUId;
|
||||
use addons\shopro\library\chat\traits\sender\SenderFunc;
|
||||
use PHPSocketIO\SocketIO;
|
||||
use PHPSocketIO\Socket;
|
||||
use PHPSocketIO\Nsp;
|
||||
|
||||
/**
|
||||
* 客服 Sender 发送类
|
||||
*/
|
||||
class Sender
|
||||
{
|
||||
|
||||
/**
|
||||
* 绑定 UID 助手
|
||||
*/
|
||||
use BindUId;
|
||||
|
||||
/**
|
||||
* 特定的发送方法
|
||||
*/
|
||||
use SenderFunc;
|
||||
/**
|
||||
* session
|
||||
*/
|
||||
use Session;
|
||||
/**
|
||||
* 帮助方法
|
||||
*/
|
||||
use Helper;
|
||||
|
||||
/**
|
||||
* 当前 socket 实例
|
||||
*
|
||||
* @var Socket
|
||||
*/
|
||||
protected $socket = null;
|
||||
/**
|
||||
* 当前 phpsocket.io 实例
|
||||
*
|
||||
* @var SocketIO
|
||||
*/
|
||||
protected $io = null;
|
||||
|
||||
/**
|
||||
* 当前 phpsocket.io 的 nsp 实例
|
||||
*
|
||||
* @var Nsp
|
||||
*/
|
||||
protected $nsp = null;
|
||||
/**
|
||||
* 当前 获取 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $getter = null;
|
||||
|
||||
|
||||
public function __construct(Socket $socket = null, SocketIo $io, Nsp $nsp, Getter $getter = null)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->io = $io;
|
||||
$this->nsp = $nsp;
|
||||
$this->getter = $getter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 返回成功方法
|
||||
*
|
||||
* @param string $event
|
||||
* @param string $msg
|
||||
* @param array $data
|
||||
* @param Socket|Nsp $data
|
||||
* @return void
|
||||
*/
|
||||
public function success($event, $msg = '', $data = null, $sender = null)
|
||||
{
|
||||
$result = [
|
||||
'code' => 1,
|
||||
'msg' => $msg,
|
||||
'data' => $data
|
||||
];
|
||||
|
||||
if ($event instanceof \Closure) {
|
||||
$event($result);
|
||||
} else {
|
||||
$sender && $sender->emit($event, $result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回成功方法
|
||||
*
|
||||
* @param string $event
|
||||
* @param string $msg
|
||||
* @param Socket|Nsp $data
|
||||
* @return void
|
||||
*/
|
||||
public function error($event, $msg = '', $data = null, $sender = null)
|
||||
{
|
||||
$result = [
|
||||
'code' => 0,
|
||||
'msg' => $msg,
|
||||
'data' => $data
|
||||
];
|
||||
|
||||
if ($event instanceof \Closure) {
|
||||
$event($result);
|
||||
} else {
|
||||
$sender && $sender->emit($event, $result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 成功发给当前渠道
|
||||
*
|
||||
* @param string $event
|
||||
* @param string $msg
|
||||
* @param Socket|Nsp $data
|
||||
* @return void
|
||||
*/
|
||||
public function successSocket($event, $msg = '', $data = null)
|
||||
{
|
||||
$this->success($event, $msg, $data, $this->socket);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 失败发给当前渠道
|
||||
*
|
||||
* @param string $event
|
||||
* @param string $msg
|
||||
* @param Socket|Nsp $data
|
||||
* @return void
|
||||
*/
|
||||
public function errorSocket($event, $msg = '', $data = null)
|
||||
{
|
||||
$this->error($event, $msg, $data, $this->socket);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 clients 发送成功事件
|
||||
*
|
||||
* @param string $event
|
||||
* @param string $msg
|
||||
* @param array $data
|
||||
* @param array $clientIds
|
||||
* @return void
|
||||
*/
|
||||
public function successClients($event, $msg = '', $data = null, $clientIds = [])
|
||||
{
|
||||
$clientIds = $clientIds ? (is_array($clientIds) ? $clientIds : [$clientIds]) : [];
|
||||
|
||||
foreach ($clientIds as $client_id) {
|
||||
if (isset($this->nsp->connected[$client_id])) {
|
||||
$sender = $this->nsp->connected[$client_id];
|
||||
$this->success($event, $msg, $data, $sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 UId 发送成功事件
|
||||
*
|
||||
* @param string $event
|
||||
* @param string $msg
|
||||
* @param array $data
|
||||
* @param array $uIdData
|
||||
* @return void
|
||||
*/
|
||||
public function successUId($event, $msg = '', $data = null, $uIdData = [])
|
||||
{
|
||||
$uIdData = $uIdData ? $uIdData : [];
|
||||
|
||||
if ($uIdData) {
|
||||
$clientIds = $this->getClientIdByUId($uIdData['id'], $uIdData['type']);
|
||||
|
||||
$this->successClients($event, $msg, $data, $clientIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 UId 发送成功事件
|
||||
*
|
||||
* @param string $event
|
||||
* @param string $msg
|
||||
* @param array $data
|
||||
* @param string $room
|
||||
* @return void
|
||||
*/
|
||||
public function successRoom($event, $msg = '', $data = null, $room = '')
|
||||
{
|
||||
$room = $room ? $room : '';
|
||||
|
||||
if ($room) {
|
||||
$sender = $this->nsp->to($room);
|
||||
$this->success($event, $msg, $data, $sender);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转发, message 开头保存数据库
|
||||
*/
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
$currentName = $name;
|
||||
|
||||
// 需要存储数据库的消息,先存储数据库,再发送
|
||||
if (strpos($name, 'message') === 0) {
|
||||
$room_id = $this->session('room_id');
|
||||
// 存库
|
||||
$chatRecord = $this->getter('db')->addMessage($room_id, $name, $arguments);
|
||||
|
||||
// 将 message 追加到 content 里面
|
||||
$content = $arguments[2] ?? [];
|
||||
$content['message'] = $chatRecord->toArray();
|
||||
$arguments[2] = $content;
|
||||
|
||||
// 重载方法名
|
||||
$currentName = str_replace('message', 'success', $name);
|
||||
}
|
||||
|
||||
|
||||
$sender_status = true;
|
||||
switch($currentName) {
|
||||
case 'successUId':
|
||||
if (!isset($arguments[3]) || !isset($arguments[3]['id']) || !$arguments[3]['id']) {
|
||||
// 缺少参数 id
|
||||
$sender_status = false;
|
||||
}
|
||||
break;
|
||||
case 'successClients':
|
||||
if (!isset($arguments[3]) || !$arguments[3]) {
|
||||
// 缺少接收的 clientIds
|
||||
$sender_status = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($sender_status) {
|
||||
// 接收者正常
|
||||
return self::$currentName(...$arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
130
addons/shopro/library/chat/provider/auth/Admin.php
Normal file
130
addons/shopro/library/chat/provider/auth/Admin.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\provider\auth;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\DebugEvent;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\NspData;
|
||||
use addons\shopro\library\chat\provider\auth\traits\CustomerService;
|
||||
use addons\shopro\library\chat\Chat;
|
||||
use addons\shopro\library\chat\ChatService;
|
||||
use addons\shopro\library\chat\Getter;
|
||||
use addons\shopro\library\chat\Sender;
|
||||
use PHPSocketIO\SocketIO;
|
||||
use PHPSocketIO\Socket;
|
||||
use PHPSocketIO\Nsp;
|
||||
|
||||
/**
|
||||
* 管理员身份
|
||||
*/
|
||||
class Admin
|
||||
{
|
||||
|
||||
/**
|
||||
* debug 方式注册事件
|
||||
*/
|
||||
use DebugEvent;
|
||||
|
||||
/**
|
||||
* 助手方法
|
||||
*/
|
||||
use Helper;
|
||||
/**
|
||||
* session 存储助手
|
||||
*/
|
||||
use Session;
|
||||
/**
|
||||
* 绑定数据到 nsp
|
||||
*/
|
||||
use NspData;
|
||||
/**
|
||||
* 客服事件
|
||||
*/
|
||||
use CustomerService;
|
||||
|
||||
/**
|
||||
* 当前 Chat 实例
|
||||
*
|
||||
* @var Chat
|
||||
*/
|
||||
protected $chat;
|
||||
/**
|
||||
* 当前 phpsocket.io 实例
|
||||
*
|
||||
* @var SocketIO
|
||||
*/
|
||||
protected $io;
|
||||
/**
|
||||
* 当前socket 连接
|
||||
*
|
||||
* @var Socket
|
||||
*/
|
||||
protected $socket;
|
||||
/**
|
||||
* 当前 命名空间
|
||||
*
|
||||
* @var Nsp
|
||||
*/
|
||||
public $nsp;
|
||||
|
||||
/**
|
||||
* getter 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $getter;
|
||||
/**
|
||||
* sender 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $sender;
|
||||
/**
|
||||
* getter 实例
|
||||
*
|
||||
* @var ChatService
|
||||
*/
|
||||
protected $chatService;
|
||||
|
||||
public function __construct(Chat $chat)
|
||||
{
|
||||
$this->chat = $chat;
|
||||
|
||||
$this->io = $chat->io;
|
||||
$this->socket = $chat->socket;
|
||||
$this->nsp = $chat->nsp;
|
||||
$this->getter = $chat->getter;
|
||||
$this->chatService = $chat->chatService;
|
||||
$this->sender = $chat->sender;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function on()
|
||||
{
|
||||
// 检测当前 auth 是否是客服
|
||||
$this->register('check_identify', function ($data, $callback) {
|
||||
// 检查当前管理员登录状态
|
||||
if (!$this->chatService->isLogin()) {
|
||||
throw new ShoproException('请先登录管理后台');
|
||||
}
|
||||
|
||||
$admin = $this->session('auth_user');
|
||||
// 获取当前管理员的客服身份
|
||||
$customerServices = $this->chatService->getCustomerServicesByAuth($admin['id'], $this->session('auth'));
|
||||
if (!$customerServices) {
|
||||
throw new ShoproException(''); // 您还不是客服,后台不提示,留空
|
||||
}
|
||||
|
||||
// 注册客服事件
|
||||
$this->customerServiceOn();
|
||||
|
||||
$this->sender->successSocket($callback, '验证成功', [
|
||||
'customer_services' => $customerServices
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
121
addons/shopro/library/chat/provider/auth/User.php
Normal file
121
addons/shopro/library/chat/provider/auth/User.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\provider\auth;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\DebugEvent;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\NspData;
|
||||
use addons\shopro\library\chat\provider\auth\traits\Customer;
|
||||
use addons\shopro\library\chat\Chat;
|
||||
use addons\shopro\library\chat\Sender;
|
||||
|
||||
|
||||
/**
|
||||
* 用户
|
||||
*/
|
||||
class User
|
||||
{
|
||||
|
||||
/**
|
||||
* debug 方式注册事件
|
||||
*/
|
||||
use DebugEvent;
|
||||
|
||||
/**
|
||||
* session 存储助手
|
||||
*/
|
||||
use Session;
|
||||
/**
|
||||
* 助手方法
|
||||
*/
|
||||
use Helper;
|
||||
|
||||
/**
|
||||
* 顾客事件
|
||||
*/
|
||||
use Customer;
|
||||
/**
|
||||
* 绑定数据到 nsp
|
||||
*/
|
||||
use NspData;
|
||||
|
||||
/**
|
||||
* 当前 Chat 实例
|
||||
*
|
||||
* @var Chat
|
||||
*/
|
||||
protected $chat;
|
||||
/**
|
||||
* 当前 phpsocket.io 实例
|
||||
*
|
||||
* @var SocketIO
|
||||
*/
|
||||
protected $io;
|
||||
/**
|
||||
* 当前socket 连接
|
||||
*
|
||||
* @var Socket
|
||||
*/
|
||||
protected $socket;
|
||||
/**
|
||||
* 当前 命名空间
|
||||
*
|
||||
* @var Nsp
|
||||
*/
|
||||
public $nsp;
|
||||
|
||||
/**
|
||||
* getter 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $getter;
|
||||
/**
|
||||
* sender 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $sender;
|
||||
/**
|
||||
* getter 实例
|
||||
*
|
||||
* @var ChatService
|
||||
*/
|
||||
protected $chatService;
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*
|
||||
* @param Chat $chat
|
||||
*/
|
||||
public function __construct(Chat $chat)
|
||||
{
|
||||
$this->chat = $chat;
|
||||
|
||||
$this->io = $chat->io;
|
||||
$this->socket = $chat->socket;
|
||||
$this->nsp = $chat->nsp;
|
||||
$this->getter = $chat->getter;
|
||||
$this->chatService = $chat->chatService;
|
||||
$this->sender = $chat->sender;
|
||||
}
|
||||
|
||||
|
||||
public function on()
|
||||
{
|
||||
// 用户相关事件
|
||||
|
||||
// 处理用户登录,将用户未登录时候产生的 连接,聊天记录,更新成当前用户
|
||||
// 更新信息
|
||||
|
||||
// 直接注册顾客相关事件
|
||||
$this->customerOn();
|
||||
|
||||
|
||||
// 用户事件,待补充
|
||||
// ......
|
||||
}
|
||||
|
||||
}
|
||||
164
addons/shopro/library/chat/provider/auth/traits/Customer.php
Normal file
164
addons/shopro/library/chat/provider/auth/traits/Customer.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\provider\auth\traits;
|
||||
|
||||
/**
|
||||
* 顾客事件
|
||||
*/
|
||||
trait Customer
|
||||
{
|
||||
public function customerOn()
|
||||
{
|
||||
// 顾客连接客服事件
|
||||
$this->register('customer_login', function ($data, $callback) {
|
||||
// 加入的客服房间
|
||||
$room_id = $data['room_id'] ?? 'admin';
|
||||
$this->session('room_id', $room_id);
|
||||
|
||||
// 存储当前房间
|
||||
$this->nspRoomIdAdd($room_id);
|
||||
|
||||
// 顾客上线
|
||||
$this->chatService->customerOnline();
|
||||
|
||||
// 通知所有房间中的客服,用户上线了
|
||||
$this->sender->customerOnline();
|
||||
|
||||
// 注册顾客相关的事件
|
||||
$this->customerEvent();
|
||||
|
||||
// 获取常见问题,提醒给顾客
|
||||
$questions = $this->getter('db')->getChatQuestions($room_id);
|
||||
// 通知自己连接成功
|
||||
$this->sender->successSocket($callback, '顾客初始成功', [
|
||||
'questions' => $questions
|
||||
]);
|
||||
|
||||
// 分配客服
|
||||
$this->allocatCustomerService();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public function customerEvent()
|
||||
{
|
||||
// 检测是否登录过
|
||||
if ($this->socket->listeners('message')) {
|
||||
// 已经注册过了,避免重复注册
|
||||
return false;
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
$this->register('message', function ($data, $callback) {
|
||||
$message = $data['message'] ?? []; // 发送的消息
|
||||
$question_id = $data['question_id'] ?? 0; // 猜你想问
|
||||
// 过滤 message
|
||||
if ($message['message_type'] == 'text') {
|
||||
$message['message'] = trim(strip_tags($message['message']));
|
||||
}
|
||||
|
||||
$room_id = $this->session('room_id');
|
||||
$session_id = $this->session('session_id');
|
||||
$customer_service_id = $this->session('customer_service_id');
|
||||
|
||||
// 给客服发消息
|
||||
$this->sender->messageToCustomerService($message, [
|
||||
'sender_identify' => 'customer',
|
||||
'session_id' => $session_id,
|
||||
], $customer_service_id);
|
||||
|
||||
// 通知自己发送成功
|
||||
$this->sender->successSocket($callback, '发送成功');
|
||||
|
||||
// 检查并获取 question
|
||||
$question = $this->getter('db')->getChatQuestion($room_id, $question_id);
|
||||
if ($question) {
|
||||
// 是猜你想问,自动回答问题
|
||||
$this->sender->messageToBoth($question, $session_id, $customer_service_id);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 获取消息列表
|
||||
$this->register('messages', function ($data, $callback) {
|
||||
// 当前房间
|
||||
$room_id = $this->session('room_id');
|
||||
$session_id = $this->session('session_id');
|
||||
|
||||
// 获取 聊天记录
|
||||
$messages = $this->getter('db')->getCustomerMessagesBySessionId($room_id, $session_id, 'customer_service', $data);
|
||||
|
||||
// 获取消息列表
|
||||
$this->sender->successSocket($callback, '获取成功', [
|
||||
'messages' => $messages
|
||||
]);
|
||||
});
|
||||
|
||||
// 顾客退出
|
||||
$this->register('customer_logout', function ($data, $callback) {
|
||||
$session_id = $this->session('session_id');
|
||||
$room_id = $this->session('room_id');
|
||||
$chatUser = $this->session('chat_user');
|
||||
|
||||
// 顾客下线
|
||||
$this->chatService->customerOffline();
|
||||
|
||||
// 顾客断开连接
|
||||
if (!$this->getter('socket')->isOnLineCustomerById($session_id)) {
|
||||
// 当前所遇用户端都断开了
|
||||
|
||||
// 这里顾客的所有客户端都断开了,在排队排名中移除
|
||||
$this->nspWaitingDel($room_id, $session_id);
|
||||
|
||||
// 排队发生变化,通知房间中所有排队用户
|
||||
$this->sender->allWaitingQueue($room_id);
|
||||
|
||||
// 通知所有房间中的客服,顾客下线
|
||||
$this->sender->customerOffline();
|
||||
}
|
||||
|
||||
// 解绑顾客相关的事件,等下次顾客在登录时再重新绑定 【customerEvent 方法绑定的所有事件】
|
||||
$this->socket->removeAllListeners('message');
|
||||
$this->socket->removeAllListeners('messages');
|
||||
$this->socket->removeAllListeners('customer_logout');
|
||||
|
||||
$this->sender->successSocket($callback, '顾客退出成功');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 分配客服
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function allocatCustomerService()
|
||||
{
|
||||
$room_id = $this->session('room_id');
|
||||
$session_id = $this->session('session_id');
|
||||
$auth = $this->session('auth');
|
||||
|
||||
// 检测并分配客服
|
||||
$customerService = $this->chatService->checkAndAllocatCustomerService();
|
||||
|
||||
if ($customerService) {
|
||||
// 将用户 session_id 从等待排名中移除(这个用户的所有客户端都会被接入)
|
||||
$this->nspWaitingDel($room_id, $session_id);
|
||||
|
||||
// 排队发生变化,通知房间中所有排队用户
|
||||
$this->sender->allWaitingQueue($room_id);
|
||||
|
||||
// 顾客被接入,通知所有自己的客户端被接入,通知房间中所有客服用户被接入(等待中移除),通知新客服,新用户接入
|
||||
$this->sender->customerAccessed($room_id, $session_id, $customerService);
|
||||
} else {
|
||||
if ($this->getter('socket')->hasCustomerServiceByRoomId($room_id)) {
|
||||
// 有客服
|
||||
$this->sender->waiting();
|
||||
} else {
|
||||
// 通知用户没有客服在线
|
||||
$this->sender->noCustomerService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\provider\auth\traits;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\DebugEvent;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
|
||||
/**
|
||||
* 客服事件
|
||||
*/
|
||||
trait CustomerService
|
||||
{
|
||||
|
||||
/**
|
||||
* debug 方式注册事件
|
||||
*/
|
||||
use DebugEvent;
|
||||
|
||||
/**
|
||||
* 助手方法
|
||||
*/
|
||||
use Helper;
|
||||
/**
|
||||
* session 存储助手
|
||||
*/
|
||||
use Session;
|
||||
|
||||
|
||||
public function customerServiceOn()
|
||||
{
|
||||
|
||||
// 客服登录
|
||||
$this->register('customer_service_login', function ($data, $callback) {
|
||||
// 加入的客服房间
|
||||
$room_id = $data['room_id'] ?? 'admin';
|
||||
$this->session('room_id', $room_id);
|
||||
|
||||
// 存储当前房间
|
||||
$this->nspRoomIdAdd($room_id);
|
||||
|
||||
// 当前管理员信息
|
||||
$auth = $this->session('auth');
|
||||
$admin = $this->session('auth_user');
|
||||
|
||||
if (!$this->chatService->customerServiceLogin($room_id, $admin['id'], $auth)) {
|
||||
throw new ShoproException('客服登录失败');
|
||||
}
|
||||
|
||||
// 注册链接上客服事件
|
||||
$this->customerServiceEvent();
|
||||
// 客服上线
|
||||
$this->customerServiceOnline();
|
||||
|
||||
// 获取客服常用语
|
||||
$commonWords = $this->getter('db')->getChatCommonWords($room_id);
|
||||
// 通知自己登录成功
|
||||
$this->sender->successSocket($callback, '客服登录成功', [
|
||||
'customer_service' => $this->session('customer_service'),
|
||||
'common_words' => $commonWords
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 客服的所有事件
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceEvent()
|
||||
{
|
||||
// 检测是否登录过
|
||||
if ($this->socket->listeners('customer_service_init')) {
|
||||
// 已经注册过了,避免重复注册
|
||||
return false;
|
||||
}
|
||||
|
||||
// 客服登录之后的所有事件
|
||||
// 客服初始化
|
||||
$this->register('customer_service_init', function ($data, $callback) {
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 获取当前客服正在服务的顾客
|
||||
$chatUsers = $this->getter()->getCustomersIngFormatByCustomerService($room_id, $customerService['id']);
|
||||
$ingChatUserIds = array_column($chatUsers, 'id');
|
||||
|
||||
// 获取等待中的顾客
|
||||
$waitings = $this->getter()->getCustomersFormatWaiting($room_id);
|
||||
|
||||
// 获取客服历史服务过的顾客
|
||||
$histories = $this->getter()->getCustomersHistoryFormatByCustomerService($room_id, $customerService['id'], $ingChatUserIds);
|
||||
|
||||
$this->sender->successSocket($callback, '初始化成功', [
|
||||
'onlines' => $chatUsers,
|
||||
'histories' => $histories,
|
||||
'waitings' => $waitings,
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// 发送消息
|
||||
$this->register('message', function ($data, $callback) {
|
||||
$session_id = $data['session_id'] ?? ''; // 接收者
|
||||
$message = $data['message'] ?? []; // 发送的消息
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 给用户发送消息
|
||||
$this->sender->messageToCustomer($message, [
|
||||
'sender_identify' => 'customer_service',
|
||||
'customer_service' => $customerService
|
||||
], $session_id);
|
||||
|
||||
// 通知自己发送成功
|
||||
$this->sender->successSocket($callback, '发送成功');
|
||||
});
|
||||
|
||||
// 获取消息列表
|
||||
$this->register('messages', function ($data, $callback) {
|
||||
// 当前房间
|
||||
$room_id = $this->session('room_id');
|
||||
|
||||
$session_id = $data['session_id']; // 要获取的顾客
|
||||
|
||||
// 获取 顾客 聊天记录
|
||||
$messages = $this->getter('db')->getCustomerMessagesBySessionId($room_id, $session_id, 'customer', $data);
|
||||
|
||||
// 获取消息列表
|
||||
$this->sender->successSocket($callback, '获取成功', [
|
||||
'messages' => $messages
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// 接入用户
|
||||
$this->register('access', function ($data, $callback) {
|
||||
$session_id = $data['session_id']; // 要接入的顾客
|
||||
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 将当前 用户与 客服绑定
|
||||
$this->chatService->bindCustomerServiceBySessionId($room_id, $session_id, $customerService);
|
||||
|
||||
// 将用户 session_id 从等待排名中移除(这个用户的所有客户端都会被接入)
|
||||
$this->nspWaitingDel($room_id, $session_id);
|
||||
|
||||
// 排队发生变化,通知房间中所有排队用户
|
||||
$this->sender->allWaitingQueue($room_id);
|
||||
|
||||
// 顾客被接入,通知所有自己的客户端被接入,通知房间中所有客服用户被接入(等待中移除),通知新客服,新用户接入
|
||||
$this->sender->customerAccessed($room_id, $session_id, $customerService);
|
||||
|
||||
// 获取消息列表
|
||||
$this->sender->successSocket($callback, '接入成功');
|
||||
});
|
||||
|
||||
|
||||
// 转接
|
||||
$this->register('transfer', function ($data, $callback) {
|
||||
// 要转接的顾客
|
||||
$session_id = $data['session_id'] ?? 0;
|
||||
// 要转接的客服 id
|
||||
$new_customer_service_id = $data['customer_service_id'] ?? 0;
|
||||
// 当前客服信息
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
if (!$new_customer_service_id) {
|
||||
// 没有传入转接客服 id
|
||||
throw new ShoproException('请选择要转接的客服');
|
||||
}
|
||||
|
||||
// 不能转接给自己
|
||||
if ($new_customer_service_id == $customerService['id']) {
|
||||
// 不能转接给自己
|
||||
throw new ShoproException('您不能转接给自己');
|
||||
}
|
||||
|
||||
// 获取被转接入的客服, 自动只取客服信息,过滤重复
|
||||
$newCustomerService = $this->getter('socket')->getCustomerServiceById($room_id, $new_customer_service_id);
|
||||
|
||||
if (!$newCustomerService) {
|
||||
throw new ShoproException('转接的客服不在线');
|
||||
}
|
||||
|
||||
// 转接客户,加入新客服,移除老客服
|
||||
$this->chatService->transferCustomerServiceBySessionId($room_id, $session_id, $customerService, $newCustomerService);
|
||||
|
||||
// 将用户 session_id 从等待排名中移除(这个用户的所有客户端都会被接入)
|
||||
$this->nspWaitingDel($room_id, $session_id);
|
||||
|
||||
// 排队发生变化,通知房间中所有排队用户
|
||||
$this->sender->allWaitingQueue($room_id);
|
||||
|
||||
// 顾客被接入,通知所有自己的客户端被接入,通知房间中所有客服用户被接入(等待中移除),通知新客服,新用户接入
|
||||
$this->sender->customerTransfer($room_id, $session_id, $customerService, $newCustomerService);
|
||||
|
||||
// 通知老客服,转接成功
|
||||
$this->sender->successSocket($callback, '转接成功');
|
||||
});
|
||||
|
||||
|
||||
// 断开连接中的顾客
|
||||
$this->register('break_customer', function ($data, $callback) {
|
||||
// 当前客服信息
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
// 要断开的顾客
|
||||
$session_id = $data['session_id'];
|
||||
|
||||
// 结束并断开客服
|
||||
$this->chatService->breakCustomerServiceBySessionId($room_id, $session_id, $customerService);
|
||||
|
||||
// 服务结束,通知顾客客服断开连接
|
||||
$this->sender->customerServiceBreak($session_id);
|
||||
|
||||
$this->sender->successSocket($callback, '服务结束成功');
|
||||
});
|
||||
|
||||
|
||||
// 删除历史中的顾客
|
||||
$this->register('del_customer', function ($data, $callback) {
|
||||
// 当前客服信息
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
// 要删除的顾客
|
||||
$session_id = $data['session_id'];
|
||||
$is_del_record = $data['is_del_record'] ?? false; // 是否删除聊天记录
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
|
||||
if (!$chatUser) {
|
||||
throw new ShoproException('删除失败');
|
||||
}
|
||||
|
||||
$this->getter('db')->delCustomerByCustomerService($room_id, $chatUser['id'], $customerService, $is_del_record);
|
||||
|
||||
$this->sender->successSocket($callback, '删除成功');
|
||||
});
|
||||
|
||||
|
||||
$this->register('del_customer_all', function ($data, $callback) {
|
||||
// 当前客服信息
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
$is_del_record = $data['is_del_record'] ?? false; // 是否删除聊天记录
|
||||
|
||||
$this->getter('db')->delCustomerAllByCustomerService($room_id, $customerService, $is_del_record);
|
||||
|
||||
$this->sender->successSocket($callback, '删除成功');
|
||||
});
|
||||
|
||||
|
||||
// 客服上线
|
||||
$this->register('customer_service_online', function ($data, $callback) {
|
||||
// 客服操作自己在线状态触发
|
||||
|
||||
// 客服上线
|
||||
$this->customerServiceOnline();
|
||||
|
||||
// 通知自己上线成功
|
||||
$this->sender->successSocket($callback, '当前状态已切换为在线', [
|
||||
'customer_service' => $this->session('customer_service')
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// 客服离线
|
||||
$this->register('customer_service_offline', function ($data, $callback) {
|
||||
// 客服操作自己为离线状态触发
|
||||
|
||||
// 客服离线
|
||||
$this->customerServiceOffline();
|
||||
|
||||
// 通知自己离线成功
|
||||
$this->sender->successSocket($callback, '当前状态已切换为离线', [
|
||||
'customer_service' => $this->session('customer_service')
|
||||
]);
|
||||
});
|
||||
|
||||
// 客服忙碌
|
||||
$this->register('customer_service_busy', function ($data, $callback) {
|
||||
// 客服操作自己在线状态触发
|
||||
|
||||
// 客服忙碌
|
||||
$this->customerServiceBusy();
|
||||
|
||||
// 通知自己离线成功
|
||||
$this->sender->successSocket($callback, '当前状态已切换为忙碌', [
|
||||
'customer_service' => $this->session('customer_service')
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// 客服登录
|
||||
$this->register('customer_service_logout', function ($data, $callback) {
|
||||
$this->customerServiceLogout();
|
||||
|
||||
// 解绑客服相关的事件,等下次客服再登录时再重新绑定
|
||||
$this->socket->removeAllListeners('customer_service_init');
|
||||
$this->socket->removeAllListeners('message');
|
||||
$this->socket->removeAllListeners('messages');
|
||||
$this->socket->removeAllListeners('access');
|
||||
$this->socket->removeAllListeners('transfer');
|
||||
$this->socket->removeAllListeners('break_customer');
|
||||
$this->socket->removeAllListeners('del_customer');
|
||||
$this->socket->removeAllListeners('del_customer_all');
|
||||
$this->socket->removeAllListeners('customer_service_online');
|
||||
$this->socket->removeAllListeners('customer_service_offline');
|
||||
$this->socket->removeAllListeners('customer_service_busy');
|
||||
$this->socket->removeAllListeners('customer_service_logout');
|
||||
|
||||
$this->sender->successSocket($callback, '退出成功');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服上线,并通知连接的用户,和房间中的其他客服
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function customerServiceOnline()
|
||||
{
|
||||
// 房间号
|
||||
$room_id = $this->session('room_id');
|
||||
// 客服信息
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 记录客服切换之前的在线状态
|
||||
$isOnLineCustomerService = $this->getter('socket')->isOnLineCustomerServiceById($customerService['id']);
|
||||
|
||||
// 客服上线,更新客服状态,加入客服组
|
||||
$this->chatService->customerServiceOnline($room_id, $customerService['id']);
|
||||
|
||||
// if (!$isOnLineCustomerService) {
|
||||
// (这里不判断,都通知,重复通知不影响)如果之前是离线状态
|
||||
|
||||
// 通知连接的用户(在当前客服服务的房间里面的用户),客服上线了
|
||||
$this->sender->customerServiceOnline();
|
||||
|
||||
// 通知当前房间的在线客服,更新当前在线客服列表
|
||||
$this->sender->customerServiceUpdate();
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 客服离线,并通知连接的用户,和房间中的其他客服
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function customerServiceOffline()
|
||||
{
|
||||
// 房间号
|
||||
$room_id = $this->session('room_id');
|
||||
// 客服信息
|
||||
$customerService = $this->session('customer_service');
|
||||
// 客服下线,更新客服状态,解绑 client_id,退出客服组
|
||||
$this->chatService->customerServiceOffline($room_id, $customerService['id']);
|
||||
|
||||
if (!$this->getter('socket')->isOnLineCustomerServiceById($customerService['id'])) {
|
||||
// 当前客服的所有客户端都下线了
|
||||
|
||||
// 通知连接的用户(在当前客服服务的房间里面的用户),客服下线了
|
||||
$this->sender->customerServiceOffline();
|
||||
|
||||
// 通知当前房间的在线客服,更新当前在线客服列表
|
||||
$this->sender->customerServiceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 客服忙碌,如果之前客服是离线 通知连接的用户,和房间中的其他客服,上线了
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function customerServiceBusy()
|
||||
{
|
||||
// 房间号
|
||||
$room_id = $this->session('room_id');
|
||||
// 客服信息
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 记录客服切换之前的在线状态
|
||||
$isOnLineCustomerService = $this->getter('socket')->isOnLineCustomerServiceById($customerService['id']);
|
||||
|
||||
// 客服忙碌,更新客服状态,判断并加入客服组
|
||||
$this->chatService->customerServiceBusy($room_id, $customerService['id']);
|
||||
|
||||
// if (!$isOnLineCustomerService) {
|
||||
// (这里不判断,都通知,重复通知不影响)如果之前是离线状态
|
||||
|
||||
// 通知连接的用户(在当前客服服务的房间里面的用户),客服上线了
|
||||
$this->sender->customerServiceBusy();
|
||||
|
||||
// 通知当前房间的在线客服,更新当前在线客服列表
|
||||
$this->sender->customerServiceUpdate();
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 客服退出登录
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceLogout()
|
||||
{
|
||||
// 房间号
|
||||
$room_id = $this->session('room_id');
|
||||
// 客服信息
|
||||
$customerService = $this->session('customer_service');
|
||||
$customer_service_id = $this->session('customer_service_id');
|
||||
|
||||
// 客服先离线
|
||||
$this->chatService->customerServiceOffline($room_id, $customer_service_id);
|
||||
|
||||
if (!$this->getter('socket')->isOnLineCustomerServiceById($customerService['id'])) {
|
||||
// 当前客服的所有客户端都下线了
|
||||
|
||||
// 通知连接的用户(在当前客服服务的房间里面的用户),客服下线了
|
||||
$this->sender->customerServiceOffline();
|
||||
// 通知当前房间的在线客服,更新当前在线客服列表
|
||||
$this->sender->customerServiceUpdate();
|
||||
}
|
||||
|
||||
// 客服退出房间,清除客服 session 信息
|
||||
$this->chatService->customerServiceLogout($room_id, $customer_service_id);
|
||||
|
||||
}
|
||||
}
|
||||
641
addons/shopro/library/chat/provider/getter/Db.php
Normal file
641
addons/shopro/library/chat/provider/getter/Db.php
Normal file
@@ -0,0 +1,641 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\provider\getter;
|
||||
|
||||
use think\helper\Str;
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\DebugEvent;
|
||||
use addons\shopro\library\chat\Getter;
|
||||
use addons\shopro\controller\traits\UnifiedToken;
|
||||
use app\admin\model\shopro\chat\User as ChatUser;
|
||||
use app\admin\model\shopro\chat\ServiceLog as ChatServiceLog;
|
||||
use app\admin\model\shopro\chat\Record as ChatRecord;
|
||||
use app\admin\model\shopro\chat\CustomerService as ChatCustomerService;
|
||||
use app\admin\model\shopro\chat\CustomerServiceUser as ChatCustomerServiceUser;
|
||||
use app\admin\model\shopro\chat\Question as ChatQuestion;
|
||||
use app\admin\model\shopro\chat\CommonWord as ChatCommonWord;
|
||||
|
||||
/**
|
||||
* 从数据库获取
|
||||
*/
|
||||
class Db
|
||||
{
|
||||
|
||||
/**
|
||||
* token 验证助手
|
||||
*/
|
||||
use UnifiedToken;
|
||||
|
||||
/**
|
||||
* getter 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $getter;
|
||||
|
||||
/**
|
||||
* 实例化的模型数组
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $models = [];
|
||||
|
||||
public function __construct(Getter $getter)
|
||||
{
|
||||
$this->getter = $getter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取动态模型实例
|
||||
*
|
||||
* @param string $type 动态类型
|
||||
* @param string $model 模型名称
|
||||
* @param boolean $is_instance 是否实例化
|
||||
* @return string|object 返回模型
|
||||
*/
|
||||
public function getModel($type, $model, $is_instance = true)
|
||||
{
|
||||
$key = $type . '_' . $model . '_' . strval($is_instance);
|
||||
if (isset($this->models[$key])) {
|
||||
return $this->models[$key];
|
||||
}
|
||||
|
||||
switch($type) {
|
||||
case 'auth' :
|
||||
if ($model == 'user') {
|
||||
$class = '\\app\\admin\\model\\shopro\\user\\' . ucfirst($model);
|
||||
} else {
|
||||
$class = '\\app\\admin\\model\\shopro\\' . ucfirst($model);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($is_instance) {
|
||||
$class = new $class;
|
||||
}
|
||||
|
||||
$this->models[$key] = $class;
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过token 获取用户 id
|
||||
*
|
||||
* @param string $token
|
||||
* @param string $auth 认证类型
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAuthIdFromToken($token, $auth)
|
||||
{
|
||||
// 获取加密数据
|
||||
$content = $this->getUnifiedContent($token);
|
||||
// 判断并返回用户 id
|
||||
if ($content && strpos($content, $auth) !== false) {
|
||||
return str_replace($auth . ':', '', $content);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 token 获取 用户信息
|
||||
*
|
||||
* @param string $token
|
||||
* @param string $auth 认证类型
|
||||
* @return null|object
|
||||
*/
|
||||
public function getAuthByToken($token, $auth)
|
||||
{
|
||||
// 获取用户 id
|
||||
$user_id = $this->getAuthIdFromToken($token, $auth);
|
||||
|
||||
// 获取用户
|
||||
return $this->getAuthById($user_id, $auth);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 id 获取用户
|
||||
*
|
||||
* @param int $id
|
||||
* @param string $auth
|
||||
* @return null|object
|
||||
*/
|
||||
public function getAuthById($id, $auth)
|
||||
{
|
||||
return $this->getModel('auth', $auth)->where('id', $id)->find();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 session_id 获取chat 用户
|
||||
*
|
||||
* @param string $session_id
|
||||
* @param string $auth 认证类型
|
||||
* @return null|object
|
||||
*/
|
||||
public function getChatUserBySessionId($session_id)
|
||||
{
|
||||
$chatUser = ChatUser::where('session_id', $session_id)->order('id asc')->find();
|
||||
|
||||
return $chatUser;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 auth 获取 library 用户信息
|
||||
*
|
||||
* @param integer $auth_id
|
||||
* @param string $auth
|
||||
* @return void
|
||||
*/
|
||||
public function getChatUserByAuth($auth_id, $auth)
|
||||
{
|
||||
$chatUser = ChatUser::where('auth', $auth)->where('auth_id', $auth_id)->order('id asc')->find();
|
||||
|
||||
return $chatUser;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取最后一次被服务记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $chat_user_id
|
||||
* @return object|null
|
||||
*/
|
||||
public function getLastServiceLogByChatUser($room_id, $chat_user_id)
|
||||
{
|
||||
return ChatServiceLog::where('room_id', $room_id)
|
||||
->where('chat_user_id', $chat_user_id)
|
||||
->where('customer_service_id', '<>', 0)
|
||||
->order('id', 'desc')->find();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取郑子昂服务中的记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $chat_user_id
|
||||
* @return object
|
||||
*/
|
||||
public function getServiceLogIngByChatUser($room_id, $chat_user_id, $customer_service_id = 0)
|
||||
{
|
||||
return ChatServiceLog::where('room_id', $room_id)
|
||||
->where('chat_user_id', $chat_user_id)
|
||||
->where(function ($query) use ($customer_service_id) {
|
||||
$query->where(function($query) use ($customer_service_id) {
|
||||
$query->where('status', 'waiting')->where('customer_service_id', 0);
|
||||
})->whereOr(function ($query) use ($customer_service_id) {
|
||||
$query->where('status', 'ing')->where('customer_service_id', $customer_service_id);
|
||||
});
|
||||
})
|
||||
->order('id', 'desc')->find();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 添加服务记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param array $chatUser
|
||||
* @param array $customerService
|
||||
* @param string $status
|
||||
* @return void
|
||||
*/
|
||||
public function addServiceLog($room_id, $chatUser, $customerService, $status)
|
||||
{
|
||||
$chatServiceLog = new ChatServiceLog();
|
||||
|
||||
$chatServiceLog->chat_user_id = $chatUser ? $chatUser['id'] : 0;
|
||||
$chatServiceLog->customer_service_id = $customerService ? $customerService['id'] : 0;
|
||||
$chatServiceLog->room_id = $room_id;
|
||||
$chatServiceLog->starttime = time();
|
||||
$chatServiceLog->endtime = $status == 'end' ? time() : null;
|
||||
$chatServiceLog->status = $status;
|
||||
$chatServiceLog->save();
|
||||
|
||||
return $chatServiceLog;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建正在进行中的 服务
|
||||
*
|
||||
* @param [type] $room_id
|
||||
* @param [type] $chatUser
|
||||
* @param [type] $customerService
|
||||
* @return void
|
||||
*/
|
||||
public function createServiceLog($room_id, $chatUser, $customerService)
|
||||
{
|
||||
// 正在进行中的连接
|
||||
$serviceLog = $this->getServiceLogIngByChatUser($room_id, $chatUser['id'], $customerService['id']);
|
||||
|
||||
if (!$serviceLog) {
|
||||
// 不存在,创建新的
|
||||
$serviceLog = $this->addServiceLog($room_id, $chatUser, $customerService, 'ing');
|
||||
}
|
||||
|
||||
return $serviceLog;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 结束 服务
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param array $chatUser 顾客
|
||||
* @param array $customerService 客服
|
||||
* @return object
|
||||
*/
|
||||
public function endServiceLog($room_id, $chatUser, $customerService)
|
||||
{
|
||||
// 正在进行中的连接
|
||||
$serviceLog = $this->getServiceLogIngByChatUser($room_id, $chatUser['id'], $customerService['id']);
|
||||
|
||||
if (!$serviceLog) {
|
||||
// 不存在,创建新的
|
||||
$serviceLog = $this->addServiceLog($room_id, $chatUser, $customerService, 'end');
|
||||
} else {
|
||||
$serviceLog->customer_service_id = $customerService ? $customerService['id'] : 0;
|
||||
$serviceLog->endtime = time();
|
||||
$serviceLog->status = 'end';
|
||||
$serviceLog->save();
|
||||
}
|
||||
|
||||
return $serviceLog;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过用户获取到用户绑定的在指定房间的客服信息(判断 auth 在指定房间是否是客服,是了才能登录客服)
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $auth 用户类型
|
||||
* @param integer $auth_id 用户 id
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCustomerServiceByAuthAndRoom($room_id, $auth_id, $auth)
|
||||
{
|
||||
// 获取当前 auth 绑定的所有客服的 id
|
||||
$customerServiceIds = ChatCustomerServiceUser::where('auth', $auth)->where('auth_id', $auth_id)->column('customer_service_id');
|
||||
// 通过上一步的 客服id 配合 房间获取第一条客服(只能有一条)
|
||||
$customerService = ChatCustomerService::where('room_id', $room_id)->where('id', 'in', $customerServiceIds)->find();
|
||||
|
||||
return $customerService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过用户获取到用户绑定的所有客服信息(暂不使用,当独立登录是,让用户选择客服身份)
|
||||
*
|
||||
* @param string $auth 用户类型
|
||||
* @param integer $auth_id 用户 id
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomerServicesByAuth($auth_id, $auth, $first = false)
|
||||
{
|
||||
// 获取当前 auth 绑定的所有客服的 id
|
||||
$customerServiceIds = ChatCustomerServiceUser::where('auth', $auth)->where('auth_id', $auth_id)->column('customer_service_id');
|
||||
// 通过上一步的 客服id 配合 房间获取第一条客服(只能有一条)
|
||||
$customerServices = ChatCustomerService::where('id', 'in', $customerServiceIds)->order('id', 'asc')->select();
|
||||
|
||||
return $first ? ($customerServices[0] ?? null) : $customerServices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取客服服务的历史用户
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param integer $customer_service_id 客服 id
|
||||
* @param array $exceptIds 要排除的ids (正在服务的用户)
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomersHistoryByCustomerService($room_id, $customer_service_id, $exceptIds = [])
|
||||
{
|
||||
// $logs = ChatServiceLog::with('chat_user')
|
||||
// ->where('room_id', $room_id)
|
||||
// ->field('chat_user_id')
|
||||
// ->whereNotIn('chat_user_id', $exceptIds)
|
||||
// ->where('customer_service_id', $customer_service_id)
|
||||
// ->group('chat_user_id')
|
||||
// ->order('id', 'desc')
|
||||
// ->select();
|
||||
// $logs = collection($logs)->toArray();
|
||||
|
||||
// $chatUsers = array_column($logs, 'chat_user');
|
||||
|
||||
// 替代上面的方法,上面方法 group by 在 mysql 严格模式必须要关闭 ONLY_FULL_GROUP_BY
|
||||
$chatUsers = [];
|
||||
$logs = ChatServiceLog::with('chat_user')
|
||||
->field('id,chat_user_id')
|
||||
->where('room_id', $room_id)
|
||||
->whereNotIn('chat_user_id', $exceptIds)
|
||||
->where('customer_service_id', $customer_service_id)
|
||||
->chunk(100, function ($currentLogs) use (&$chatUsers) {
|
||||
foreach ($currentLogs as $log) {
|
||||
$chatUser = $log->chat_user;
|
||||
$currentIds = array_column($chatUsers, 'id');
|
||||
if ($chatUser && !in_array($chatUser->id, $currentIds)) {
|
||||
$chatUsers[] = $chatUser;
|
||||
}
|
||||
|
||||
if (count($chatUsers) >= 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($chatUsers) >= 20) {
|
||||
return false;
|
||||
}
|
||||
}, 'id', 'desc'); // 如果 id 重复,有坑 (date < 2020-03-28)
|
||||
|
||||
return $chatUsers;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 session_id 获取 顾客
|
||||
*
|
||||
* @param string $session_id
|
||||
* @return object
|
||||
*/
|
||||
public function getCustomerBySessionId($session_id)
|
||||
{
|
||||
return ChatUser::where('session_id', $session_id)->find();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除客服的一个顾客的所有服务记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $chat_user_id
|
||||
* @param array $customerService
|
||||
* @param boolean $is_del_record
|
||||
* @return void
|
||||
*/
|
||||
public static function delCustomerByCustomerService($room_id, $chat_user_id, $customerService, $is_del_record = false)
|
||||
{
|
||||
ChatServiceLog::where('room_id', $room_id)
|
||||
->where('customer_service_id', $customerService['id'])
|
||||
->where('chat_user_id', $chat_user_id)->delete();
|
||||
|
||||
if ($is_del_record) {
|
||||
self::delCustomerRecordById($room_id, $chat_user_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除客服的一个顾客的所有服务记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param array $customerService
|
||||
* @param boolean $is_del_record
|
||||
* @return void
|
||||
*/
|
||||
public static function delCustomerAllByCustomerService($room_id, $customerService, $is_del_record = false)
|
||||
{
|
||||
if ($is_del_record) {
|
||||
$chatUserIds = ChatServiceLog::where('room_id', $room_id)
|
||||
->where('customer_service_id', $customerService['id'])->column('chat_user_id');
|
||||
$chatUserIds = array_values(array_unique($chatUserIds));
|
||||
|
||||
foreach ($chatUserIds as $chat_user_id) {
|
||||
self::delCustomerRecordById($room_id, $chat_user_id);
|
||||
}
|
||||
}
|
||||
|
||||
ChatServiceLog::where('room_id', $room_id)
|
||||
->where('customer_service_id', $customerService['id'])->delete();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除客户聊天记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param int $chat_user_id
|
||||
* @return void
|
||||
*/
|
||||
public static function delCustomerRecordById($room_id, $chat_user_id)
|
||||
{
|
||||
ChatRecord::where('room_id', $room_id)->where('chat_user_id', $chat_user_id)->delete();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取顾客的聊天记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $session_id
|
||||
* @param $select_identify
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomerMessagesBySessionId($room_id, $session_id, $select_identify, $data)
|
||||
{
|
||||
$selectIdentify = Str::camel($select_identify);
|
||||
|
||||
$customer = $this->getCustomerBySessionId($session_id);
|
||||
$chat_user_id = $customer ? $customer['id'] : 0;
|
||||
|
||||
// 将消息标记为已读
|
||||
ChatRecord::where('room_id', $room_id)->where('chat_user_id', $chat_user_id)->{$selectIdentify}()->update([
|
||||
'read_time' => time()
|
||||
]);
|
||||
|
||||
$page = $data['page'] ?? 1;
|
||||
$list_rows = $data['list_rows'] ?? 20;
|
||||
$last_id = $data['last_id'] ?? 0;
|
||||
|
||||
$messageList = ChatRecord::where('room_id', $room_id)->where('chat_user_id', $chat_user_id);
|
||||
|
||||
// 避免消息重复
|
||||
if ($last_id) {
|
||||
$messageList = $messageList->where('id', '<=', $last_id);
|
||||
}
|
||||
|
||||
$messageList = $messageList->order('id', 'desc')->paginate([
|
||||
'page' => $page,
|
||||
'list_rows' => $list_rows
|
||||
]);
|
||||
$messageList = $this->getMessageSender($messageList);
|
||||
|
||||
return $messageList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户的最后一条消息(当前房间的)
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $chat_user_id
|
||||
* @return object
|
||||
*/
|
||||
public function getMessageLastByChatUser($room_id, $chat_user_id)
|
||||
{
|
||||
return ChatRecord::where('room_id', $room_id)->where('chat_user_id', $chat_user_id)->order('id', 'desc')->find();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据身份获取未读消息条数(当前房间的)
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param integer $chat_user_id
|
||||
* @param string $select_identify
|
||||
* @return integer
|
||||
*/
|
||||
public function getMessageUnReadNumByChatUserAndIndentify($room_id, $chat_user_id, $select_identify)
|
||||
{
|
||||
$selectIdentify = Str::camel($select_identify);
|
||||
return ChatRecord::where('room_id', $room_id)->where('chat_user_id', $chat_user_id)->where('read_time', 'null')->{$selectIdentify}()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客服信息
|
||||
*
|
||||
* @param integer $id
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function updateCustomerService($id, $data)
|
||||
{
|
||||
$customerService = ChatCustomerService::where('id', $id)->update($data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 添加消息记录
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return object
|
||||
*/
|
||||
public function addMessage($room_id, $name, $arguments)
|
||||
{
|
||||
$content = $arguments[2] ?? []; // 额外参数
|
||||
$message = $content['message'];
|
||||
$sender = $content['sender'];
|
||||
$sender_identify = $sender['sender_identify'] ?? 'customer';
|
||||
$receive = $arguments[3] ?? [];
|
||||
|
||||
if ($sender_identify == 'customer') {
|
||||
$session_id = $sender['session_id'];
|
||||
$chatUser = $this->getChatUserBySessionId($session_id);
|
||||
$sender_id = $chatUser['id'] ?? 0;
|
||||
$chat_user_id = $sender_id;
|
||||
} else {
|
||||
$customerService = $sender['customer_service'];
|
||||
$sender_id = $customerService['id'] ?? 0;
|
||||
|
||||
$session_id = $receive['id'];
|
||||
$chatUser = $this->getChatUserBySessionId($session_id);
|
||||
$chat_user_id = $chatUser['id'] ?? 0;
|
||||
}
|
||||
|
||||
$chatRecord = new ChatRecord();
|
||||
|
||||
$chatRecord->chat_user_id = $chat_user_id;
|
||||
$chatRecord->room_id = $room_id;
|
||||
$chatRecord->sender_identify = $sender_identify;
|
||||
$chatRecord->sender_id = $sender_id;
|
||||
$chatRecord->message_type = $message['message_type'] ?? 'text';
|
||||
$chatRecord->message = $message['message'] ?? '';
|
||||
$chatRecord->createtime = time();
|
||||
$chatRecord->updatetime = time();
|
||||
$chatRecord->save();
|
||||
|
||||
// 加载消息人
|
||||
$chatRecord = $this->getMessageSender([$chatRecord])[0];
|
||||
|
||||
return $chatRecord;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取猜你想问
|
||||
*
|
||||
* @param string $room_id 房间
|
||||
* @return object
|
||||
*/
|
||||
public function getChatQuestions($room_id)
|
||||
{
|
||||
$chatQuestions = ChatQuestion::roomId($room_id)->order(['weigh' => 'desc', 'id' => 'desc'])->select();
|
||||
|
||||
return $chatQuestions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据 id 获取猜你想问
|
||||
*
|
||||
* @param string $room_id 房间
|
||||
* @return object
|
||||
*/
|
||||
public function getChatQuestion($room_id, $question_id)
|
||||
{
|
||||
if ($question_id) {
|
||||
$chatQuestion = ChatQuestion::roomId($room_id)->find($question_id);
|
||||
|
||||
return $chatQuestion;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取客服常用语
|
||||
*
|
||||
* @param string $room_id 房间
|
||||
* @return object
|
||||
*/
|
||||
public function getChatCommonWords($room_id)
|
||||
{
|
||||
$chatCommonWords = ChatCommonWord::normal()->roomId($room_id)->order(['weigh' => 'desc', 'id' => 'desc'])->select();
|
||||
|
||||
return $chatCommonWords;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取消息的 发送人
|
||||
*
|
||||
* @param array|object $messageList
|
||||
* @return array|object
|
||||
*/
|
||||
private function getMessageSender($messageList)
|
||||
{
|
||||
$morphs = [
|
||||
'customer' => \app\admin\model\shopro\chat\User::class,
|
||||
'customer_service' => \app\admin\model\shopro\chat\CustomerService::class
|
||||
];
|
||||
$messageList = morph_to($messageList, $morphs, ['sender_identify', 'sender_id']);
|
||||
|
||||
return $messageList;
|
||||
}
|
||||
}
|
||||
354
addons/shopro/library/chat/provider/getter/Socket.php
Normal file
354
addons/shopro/library/chat/provider/getter/Socket.php
Normal file
@@ -0,0 +1,354 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\provider\getter;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\BindUId;
|
||||
use addons\shopro\library\chat\Getter;
|
||||
use PHPSocketIO\SocketIO;
|
||||
use PHPSocketIO\Socket as PhpSocket;
|
||||
use PHPSocketIO\Nsp;
|
||||
|
||||
/**
|
||||
* 从 socket 连接中获取
|
||||
*/
|
||||
class Socket
|
||||
{
|
||||
|
||||
/**
|
||||
* session 存储助手
|
||||
*/
|
||||
use Session;
|
||||
|
||||
/**
|
||||
* BindUid 助手
|
||||
*/
|
||||
use BindUId;
|
||||
/**
|
||||
* 助手方法
|
||||
*/
|
||||
use Helper;
|
||||
|
||||
/**
|
||||
* getter 实例
|
||||
*
|
||||
* @var Getter
|
||||
*/
|
||||
protected $getter;
|
||||
|
||||
/**
|
||||
* 当前 phpsocket.io 实例
|
||||
*
|
||||
* @var SocketIo
|
||||
*/
|
||||
protected $io = null;
|
||||
/**
|
||||
* 当前socket 连接
|
||||
*
|
||||
* @var PhpSocket
|
||||
*/
|
||||
protected $socket = null;
|
||||
/**
|
||||
* 当前 命名空间实例
|
||||
*
|
||||
* @var Nsp
|
||||
*/
|
||||
protected $nsp = null;
|
||||
|
||||
|
||||
public function __construct(Getter $getter, PhpSocket $socket = null, SocketIo $io, Nsp $nsp)
|
||||
{
|
||||
$this->getter = $getter;
|
||||
$this->socket = $socket;
|
||||
$this->io = $io;
|
||||
$this->nsp = $nsp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过房间获取房间中所有连接的 clientIds
|
||||
*
|
||||
* @param string $room 房间名称
|
||||
* @return void
|
||||
*/
|
||||
public function getClientIdsByRoom($room) {
|
||||
// 获取到的数组 键是 id 值是 boolean true
|
||||
$clientIds = $this->nsp->adapter->rooms[$room] ?? [];
|
||||
// 取出 clientIds
|
||||
$clientIds = array_keys($clientIds);
|
||||
return $clientIds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据房间获取所有房间中的 authUser
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $is_unique 默认过滤重复的数据
|
||||
* @return array
|
||||
*/
|
||||
public function getAuthsByAuth($auth, $is_unique = true)
|
||||
{
|
||||
// 要接入的客服所在的房间,默认 admin 房间,z这里的客服的状态都是 在线的,如果手动切换为离线,则会被移除该房间
|
||||
$room = $this->getRoomName('auth', ['auth' => $auth]);
|
||||
|
||||
$sessions = $this->getSessionsByRoom($room, null, $is_unique);
|
||||
|
||||
$authUsers = [];
|
||||
foreach ($sessions as $session) {
|
||||
if (isset($session['auth_user']) && $session['auth_user']) {
|
||||
$authUsers[$session['session_id']] = $session['auth_user'];
|
||||
}
|
||||
}
|
||||
|
||||
return $authUsers;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过id 获取指定客服,并且还必须在对应的客服房间中
|
||||
*
|
||||
* @param string $id 客服 id
|
||||
* @param string $room_id 客服房间号
|
||||
* @return array|null
|
||||
*/
|
||||
public function getCustomerServiceById($room_id, $id)
|
||||
{
|
||||
// 要接入的客服所在的房间,默认 admin 房间
|
||||
$room = $this->getRoomName('customer_service_room', ['room_id' => $room_id]);
|
||||
|
||||
// 房间中的所有客服的 clientids
|
||||
$roomClientIds = $this->getClientIdsByRoom($room);
|
||||
|
||||
// 当前客服 uid 绑定的所有客户端 clientIds,手动后台离线的已经被解绑了,这里都是状态为在线的
|
||||
$currentClientIds = $this->getClientIdByUId($id, 'customer_service');
|
||||
|
||||
if ($clientIds = array_intersect($currentClientIds, $roomClientIds)) {
|
||||
// 客服在线
|
||||
return $this->getSession(current($clientIds), 'customer_service');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 客服 id 判断客服是否在线,这里不管客服所在房间,一个客服只能属于一个房间
|
||||
*
|
||||
* @param string $id 客服 id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isOnLineCustomerServiceById($id)
|
||||
{
|
||||
return $this->isUIdOnline($id, 'customer_service');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据房间获取所有房间中的客服
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @param string $is_unique 默认过滤重复的数据
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomerServicesByRoomId($room_id, $is_unique = true)
|
||||
{
|
||||
// 要接入的客服所在的房间,默认 admin 房间,z这里的客服的状态都是 在线的,如果手动切换为离线,则会被移除该房间
|
||||
$room = $this->getRoomName('customer_service_room', ['room_id' => $room_id]);
|
||||
|
||||
$customerServices = $this->getSessionsByRoom($room, 'customer_service', $is_unique);
|
||||
|
||||
return $customerServices;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定 session_id 在对应房间的所有客户端的服务客服的信息
|
||||
*
|
||||
* @param [type] $room_id
|
||||
* @param [type] $session_id
|
||||
* @return void
|
||||
*/
|
||||
public function getCustomerServiceBySessionId($room_id, $session_id)
|
||||
{
|
||||
$sessions = $this->getSessionsById($session_id, 'customer');
|
||||
|
||||
$customerService = null;
|
||||
foreach ($sessions as $session) {
|
||||
if (isset($session['room_id']) && $session['room_id'] == $room_id
|
||||
&& isset($session['customer_service']) && $session['customer_service']) {
|
||||
$currentCustomerService = $session['customer_service'];
|
||||
|
||||
if ($this->isOnLineCustomerServiceById($currentCustomerService['id'])) {
|
||||
// 如果客服在线
|
||||
$customerService = $currentCustomerService;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $customerService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断当前房间中是否有客服在线
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasCustomerServiceByRoomId($room_id)
|
||||
{
|
||||
// 获取房间中所有客服的 session,只要有,就说明有客服在线,手动切换状态的会被移除 在线客服房间
|
||||
$allCustomerServices = $this->getCustomerServicesByRoomId($room_id, false);
|
||||
|
||||
if ($allCustomerServices) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断并通过 顾客 获取顾客的客服
|
||||
*/
|
||||
public function getCustomerServiceByCustomerSessionId($session_id)
|
||||
{
|
||||
$customerServices = $this->getSessionsById($session_id, 'customer', 'customer_service');
|
||||
|
||||
return current($customerServices) ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* (获取客服正在服务的顾客)根据客服获取客服房间中所有的被服务用户
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @param integer $customer_service_id 客服 id
|
||||
* @param string $is_unique 默认过滤重复的数据
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomersIngByCustomerService($room_id, $customer_service_id, $is_unique = true)
|
||||
{
|
||||
// 要接入的客服所在的房间,默认 admin 房间,z这里的客服的状态都是 在线的,如果手动切换为离线,则会被移除该房间
|
||||
$room = $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customer_service_id]);
|
||||
|
||||
$customers = $this->getSessionsByRoom($room, 'chat_user', $is_unique);
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* (获取等待被接入的用户)根据房间号获取被服务对象
|
||||
*
|
||||
* @param string $room_id 客服房间号
|
||||
* @param integer $customer_service_id 客服 id
|
||||
* @param string $is_unique 默认过滤重复的数据
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomersWaiting($room_id, $is_unique = true)
|
||||
{
|
||||
// 要接入的客服所在的房间,默认 admin 房间,z这里的客服的状态都是 在线的,如果手动切换为离线,则会被移除该房间
|
||||
$room = $this->getRoomName('customer_service_room_waiting', ['room_id' => $room_id]);
|
||||
|
||||
$customers = $this->getSessionsByRoom($room, 'chat_user', $is_unique);
|
||||
|
||||
return $customers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 session_id 判断顾客是否在线
|
||||
*
|
||||
* @param string $id 客服 id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isOnLineCustomerById($session_id)
|
||||
{
|
||||
return $this->isUIdOnline($session_id, 'customer');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 session_id 判断 auth 是否在线
|
||||
*
|
||||
* @param string $id 客服 id
|
||||
* @return boolean
|
||||
*/
|
||||
public function isOnlineAuthBySessionId($session_id, $auth)
|
||||
{
|
||||
return $this->isUIdOnline($session_id, $auth);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 id 获取这个人在 type 下的所有客户端 session 数据
|
||||
*
|
||||
* @param string $id 要获取的用户的 id
|
||||
* @param string $type bind 类型:user,admin,customer_service 等
|
||||
* @param string $name 要获取session 中的特定值,默认全部数据
|
||||
* @return array
|
||||
*/
|
||||
public function getSessionsById($id, $type, $name = null)
|
||||
{
|
||||
$currentClientIds = $this->getClientIdByUId($id, $type);
|
||||
|
||||
$sessionDatas = $this->getSessionByClientIds($currentClientIds, $name);
|
||||
|
||||
return $sessionDatas;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 id 更新 id 绑定的所有客户端的 session
|
||||
*
|
||||
* @param integer $id
|
||||
* @param string $type
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function updateSessionsById($id, $type, $data = [])
|
||||
{
|
||||
// 当前id 在当前类型下绑定的所有客户端
|
||||
$currentClientIds = $this->getClientIdByUId($id, $type);
|
||||
|
||||
$this->updateSessionByClientIds($currentClientIds, $data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取房间中的所有客户端的 session
|
||||
*
|
||||
* @param string $room 房间真实名称
|
||||
* @param string $name 要取用的 session 中的键名,默认全部取出
|
||||
* @param string $is_unique 默认根据绑定的Uid 过滤重复的 session
|
||||
* @return array
|
||||
*/
|
||||
public function getSessionsByRoom($room, $name = null, $is_unique = true) {
|
||||
// 房间中的所有客服的 clientids
|
||||
$roomClientIds = $this->getClientIdsByRoom($room);
|
||||
|
||||
$sessionDatas = $this->getSessionByClientIds($roomClientIds); // 要过滤重复,没办法直接获取指定数据
|
||||
|
||||
// 处理数据
|
||||
$newDatas = [];
|
||||
foreach ($sessionDatas as $sessionData) {
|
||||
if ($is_unique) {
|
||||
// 过滤重复
|
||||
$newDatas[$sessionData['session_id']] = $name ? $sessionData[$name] : $sessionData;
|
||||
} else {
|
||||
// 全部数据
|
||||
$newDatas[] = $name ? $sessionData[$name] : $sessionData;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_filter($newDatas));
|
||||
}
|
||||
}
|
||||
117
addons/shopro/library/chat/traits/BindUId.php
Normal file
117
addons/shopro/library/chat/traits/BindUId.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\traits;
|
||||
|
||||
/**
|
||||
* 绑定 uid
|
||||
*/
|
||||
trait BindUId
|
||||
{
|
||||
|
||||
/**
|
||||
* 绑定类型
|
||||
*/
|
||||
protected $bindType = [
|
||||
'user', // 用户
|
||||
'admin', // 管理员
|
||||
'customer', // 顾客 (用户或者管理员的身份提升)
|
||||
'customer_service' // 客服 (用户或者管理员的身份提升)
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* 根据类型获取 uid
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $type 目前有三种, user,admin,customer_service
|
||||
* @return string|array
|
||||
*/
|
||||
public function getUId($id, $type)
|
||||
{
|
||||
$ids = is_array($id) ? $id : [$id];
|
||||
foreach ($ids as &$i) {
|
||||
$i = $type . '-' . $i;
|
||||
}
|
||||
|
||||
return is_array($id) ? $ids : $ids[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绑定 uid
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function bindUId($id, $type)
|
||||
{
|
||||
$uId = $this->getUId($id, $type);
|
||||
|
||||
// 当前客户端标示
|
||||
$client_id = $this->socket->id;
|
||||
|
||||
$this->nsp->bind[$uId][$client_id] = $client_id;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 uid 获取当前 uid 绑定的所有clientid
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public function getClientIdByUId($id, $type) {
|
||||
$uId = $this->getUId($id, $type);
|
||||
|
||||
return $this->nsp->bind[$uId] ?? [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解绑 uid,将当前客户端与当前 uid 解绑,如果 uid 下没有客户端了,则将该 uid 删除
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function unbindUId($id, $type)
|
||||
{
|
||||
$uId = $this->getUId($id, $type);
|
||||
|
||||
// 当前客户端标示
|
||||
$client_id = $this->socket->id;
|
||||
|
||||
if (isset($this->nsp->bind[$uId][$client_id])) {
|
||||
unset($this->nsp->bind[$uId][$client_id]);
|
||||
|
||||
if (!$this->nsp->bind[$uId]) {
|
||||
unset($this->nsp->bind[$uId]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* UId 是否在线
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $type
|
||||
* @return boolean
|
||||
*/
|
||||
public function isUIdOnline($id, $type)
|
||||
{
|
||||
$uId = $this->getUId($id, $type);
|
||||
|
||||
if (isset($this->nsp->bind[$uId]) && $this->nsp->bind[$uId]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
75
addons/shopro/library/chat/traits/DebugEvent.php
Normal file
75
addons/shopro/library/chat/traits/DebugEvent.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\traits;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
|
||||
/**
|
||||
* debug 方式注册事件
|
||||
*/
|
||||
trait DebugEvent
|
||||
{
|
||||
|
||||
/**
|
||||
* 注册事件
|
||||
*/
|
||||
public function register($event, \Closure $cb)
|
||||
{
|
||||
$this->socket->on($event, function ($data, $callback = null) use ($cb) {
|
||||
try {
|
||||
$cb($data, $callback);
|
||||
} catch (\Exception $e) {
|
||||
$this->errorHandler($e, 'register');
|
||||
|
||||
// 将错误报告给前端
|
||||
$this->sender->errorSocket('custom_error', ($this->io->debug || $e instanceof ShoproException) ? $e->getMessage() : 'socket 服务器异常');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 执行代码
|
||||
*
|
||||
* @param object $httpConnection 当前 socket 连接
|
||||
* @param \Closure $callback
|
||||
* @return void
|
||||
*/
|
||||
public function exec($httpConnection, \Closure $callback)
|
||||
{
|
||||
try {
|
||||
$callback();
|
||||
} catch (\Exception $e) {
|
||||
$this->errorHandler($e, 'exec');
|
||||
|
||||
// 将错误报告给前端
|
||||
$httpConnection->send(($this->io->debug || $e instanceof ShoproException) ? $e->getMessage() : 'socket 服务器异常');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 判断处理异常
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
private function errorHandler(\Exception $e, $type)
|
||||
{
|
||||
$error = [
|
||||
'line' => $e->getLine(),
|
||||
'file' => $e->getFile(),
|
||||
'error' => $e->getMessage()
|
||||
// 'trace' => $e->getTrace(),
|
||||
];
|
||||
|
||||
if ($this->io->debug) {
|
||||
echo 'websocket:' . $type . ':执行失败,错误信息:' . json_encode($error, JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
format_log_error($e, 'WebSocket', 'WebSocket 执行失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
94
addons/shopro/library/chat/traits/Helper.php
Normal file
94
addons/shopro/library/chat/traits/Helper.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\traits;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\library\chat\Getter;
|
||||
|
||||
/**
|
||||
* 助手方法,需要全局地方可用
|
||||
*/
|
||||
trait Helper
|
||||
{
|
||||
|
||||
/**
|
||||
* 客服配置
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $chatConfig;
|
||||
|
||||
|
||||
/**
|
||||
* 获取客服配置
|
||||
*
|
||||
* @param string $name 要获取的配置项,不能获取第二级的配置项
|
||||
* @return void
|
||||
*/
|
||||
public function getConfig($name = null)
|
||||
{
|
||||
// 初始化 workerman 的时候不能读取数据库,会导致数据库连接异常
|
||||
if (!$this->chatConfig) {
|
||||
$config_path = ROOT_PATH . 'application' . DS . 'extra' . DS . 'chat.php';
|
||||
if (!file_exists($config_path)) {
|
||||
throw new ShoproException('客服配置文件不存在,请在后台->商城配置->客服配置,修改并保存客服配置');
|
||||
}
|
||||
|
||||
$this->chatConfig = require($config_path);
|
||||
}
|
||||
|
||||
return $name ? ($this->chatConfig[$name] ?? null) : $this->chatConfig;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据类型获取房间完整名字,主要为了记录当前系统总共有多少房间
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function getRoomName($type, $data = [])
|
||||
{
|
||||
switch ($type) {
|
||||
case 'online': // 当前在线客户端,包含所有连接着
|
||||
$group_name = 'online';
|
||||
break;
|
||||
case 'auth': // 当前用户认证分组,包含 $this->auth 的所有身份分组
|
||||
$group_name = 'auth:' . $data['auth'];
|
||||
break;
|
||||
case 'identify': // 当前身份分组,customer 顾客,customer_service 客服
|
||||
$group_name = 'identify:' . $data['identify'];
|
||||
break;
|
||||
case 'customer_service_room': // 当前在线客服数组, 这里的客服的状态都是 在线的,如果手动切换为离线,则会被移除该房间
|
||||
$group_name = 'customer_service_room:' . ($data['room_id'] ?? 'admin');
|
||||
break;
|
||||
case 'customer_service_room_waiting': // 当前在线用户所在的客服分组的等待房间种
|
||||
$group_name = 'customer_service_room_waiting:' . ($data['room_id'] ?? 'admin');
|
||||
break;
|
||||
case 'customer_service_room_user': // 当前在线用户所在的客服分组
|
||||
$group_name = 'customer_service_room_user:' . ($data['room_id'] ?? 'admin') . ':' . ($data['customer_service_id'] ?? 0);
|
||||
break;
|
||||
default:
|
||||
$group_name = $type;
|
||||
break;
|
||||
}
|
||||
|
||||
return $group_name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取 getter 实例,类中有 getter 属性才可以用
|
||||
*
|
||||
* @param string $driver
|
||||
* @return Getter
|
||||
*/
|
||||
protected function getter($driver = null)
|
||||
{
|
||||
if ($driver) {
|
||||
return $this->getter->driver($driver);
|
||||
}
|
||||
return $this->getter;
|
||||
}
|
||||
}
|
||||
302
addons/shopro/library/chat/traits/NspData.php
Normal file
302
addons/shopro/library/chat/traits/NspData.php
Normal file
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\traits;
|
||||
|
||||
/**
|
||||
* 在 nsp 连接实例绑定数据
|
||||
*/
|
||||
trait NspData
|
||||
{
|
||||
// data 的格式
|
||||
// 'data' => [
|
||||
// 'room_ids' => ['admin'] // 客服房间数组,目前只有 admin
|
||||
// 'session_ids' => ['user' => [1,2,3], 'admin' => [1,2,3]] // 登录的身份的 ids
|
||||
// 'connections' => [
|
||||
// room_id => [
|
||||
// 'session_id' => customer_service_id
|
||||
// ]
|
||||
// ],
|
||||
// 'waitings' => [
|
||||
// 'room_id' => [排队的用户]
|
||||
// ]
|
||||
// ]
|
||||
|
||||
|
||||
/**
|
||||
* 获取存储的数据,当前 Nsp
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function nspData($name = null, $value = '')
|
||||
{
|
||||
$data = $this->nsp->nspData ?? [];
|
||||
|
||||
if (is_null($name)) {
|
||||
// 获取全部数据
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ('' === $value) {
|
||||
// 获取缓存
|
||||
return 0 === strpos($name, '?') ? isset($data[$name]) : ($data[$name] ?? null);
|
||||
} elseif (is_null($value)) {
|
||||
// 删除缓存
|
||||
unset($data[$name]);
|
||||
$this->nsp->nspData = $data;
|
||||
}
|
||||
|
||||
// 存数据
|
||||
$data[$name] = $value;
|
||||
$this->nsp->nspData = $data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取等待中的排队,如果有房间,返回当前房间的
|
||||
*
|
||||
* @param string $room_id
|
||||
* @return array
|
||||
*/
|
||||
// public function nspGetSessionIds($auth)
|
||||
// {
|
||||
// $sessionIds = $this->nspData('session_ids');
|
||||
// $sessionIds = $sessionIds ?: [];
|
||||
|
||||
// return $auth ? $sessionIds[$auth] ?? [] : $sessionIds;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 保存当前在线的 auth id
|
||||
*
|
||||
* @param integer $session_id
|
||||
* @return void
|
||||
*/
|
||||
// public function nspSessionIdAdd($session_id, $auth)
|
||||
// {
|
||||
// $sessionIds = $this->nspData('session_ids');
|
||||
// $sessionIds = $sessionIds ?: [];
|
||||
|
||||
// // 当前 auth 的 ids
|
||||
// $currentsessionIds = $sessionIds[$auth] ?? [];
|
||||
|
||||
// // 已经存在直接忽略
|
||||
// if (!in_array($session_id, $currentsessionIds)) {
|
||||
// // 追加auth
|
||||
// $currentsessionIds[] = $session_id;
|
||||
|
||||
// $sessionIds[$auth] = $currentsessionIds;
|
||||
// $this->nspData('session_ids', $sessionIds);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 删除一个 auth id (离线了)
|
||||
*
|
||||
* @param integer $session_id
|
||||
* @return void
|
||||
*/
|
||||
// public function nspSessionIdDel($session_id, $auth)
|
||||
// {
|
||||
// $sessionIds = $this->nspData('session_ids');
|
||||
// $sessionIds = $sessionIds ?: [];
|
||||
|
||||
// // 当前 auth 的 ids
|
||||
// $currentsessionIds = $sessionIds[$auth] ?? [];
|
||||
|
||||
// $key = array_search($session_id, $currentsessionIds);
|
||||
// if ($key !== false) {
|
||||
// // 移除
|
||||
// unset($currentsessionIds[$key]);
|
||||
|
||||
// // 重新赋值
|
||||
// $sessionIds[$auth] = array_values($currentsessionIds);
|
||||
|
||||
// $this->nspData('session_ids', $sessionIds);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取顾客的连接客服(这个信息只会存 10s,防止用户刷新,重新分配客服问题)
|
||||
*
|
||||
* @param string $room_id
|
||||
* @return array
|
||||
*/
|
||||
public function nspGetConnectionCustomerServiceId($room_id, $session_id)
|
||||
{
|
||||
$connections = $this->nspData('connections');
|
||||
$connections = $connections ?: [];
|
||||
|
||||
// 当前房间的 waitings
|
||||
$roomConnections = $connections[$room_id] ?? [];
|
||||
|
||||
$data_str = $roomConnections[$session_id] ?? '';
|
||||
if ($data_str) {
|
||||
// 删除 connections
|
||||
unset($roomConnections[$session_id]);
|
||||
$connections[$room_id] = $roomConnections;
|
||||
$this->nspData('connections', $connections);
|
||||
|
||||
// 获取 客服 id
|
||||
$data = explode('-', $data_str);
|
||||
if ($data[0] >= (time() - 300)) {
|
||||
return $data[1] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 记录用户的服务客服
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $session_id
|
||||
* @param string $customer_service_id
|
||||
* @return void
|
||||
*/
|
||||
public function nspConnectionAdd($room_id, $session_id, $customer_service_id)
|
||||
{
|
||||
// 所有 waitings
|
||||
$connections = $this->nspData('connections');
|
||||
$connections = $connections ?: [];
|
||||
|
||||
// 当前房间的 waitings
|
||||
$roomConnections = $connections[$room_id] ?? [];
|
||||
|
||||
if (!in_array($session_id, $roomConnections)) {
|
||||
// 将 session_id 和对应的 客服 id 关联
|
||||
$roomConnections[$session_id] = time() . '-' . $customer_service_id;
|
||||
// 重新赋值
|
||||
$connections[$room_id] = $roomConnections;
|
||||
// 保存
|
||||
$this->nspData('connections', $connections);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 记录要创建的 客服 room 房间
|
||||
*
|
||||
* @param string $room_id
|
||||
* @return void
|
||||
*/
|
||||
public function nspRoomIdAdd($room_id)
|
||||
{
|
||||
$roomIds = $this->nspData('room_ids');
|
||||
$roomIds = $roomIds ?: [];
|
||||
|
||||
// 已经存在直接忽略
|
||||
if (!in_array($room_id, $roomIds)) {
|
||||
// 追加房间
|
||||
$roomIds[] = $room_id;
|
||||
$this->nspData('room_ids', $roomIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取等待中的排队,如果有房间,返回当前房间的
|
||||
*
|
||||
* @param string $room_id
|
||||
* @return array
|
||||
*/
|
||||
public function nspGetWaitings($room_id = null)
|
||||
{
|
||||
$waitings = $this->nspData('waitings');
|
||||
$waitings = $waitings ?: [];
|
||||
|
||||
return $room_id ? $waitings[$room_id] ?? [] : $waitings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 给对应房间的排队中追加顾客,如果已经存在,忽略
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $session_id
|
||||
* @return void
|
||||
*/
|
||||
public function nspWaitingAdd($room_id, $session_id)
|
||||
{
|
||||
// 所有 waitings
|
||||
$waitings = $this->nspData('waitings');
|
||||
$waitings = $waitings ? : [];
|
||||
|
||||
// 当前房间的 waitings
|
||||
$roomWaitings = $waitings[$room_id] ?? [];
|
||||
|
||||
if (!in_array($session_id, $roomWaitings)) {
|
||||
// 将 session_id 加入 房间 waitings,如果存在,忽略
|
||||
$roomWaitings[] = $session_id;
|
||||
// 重新赋值
|
||||
$waitings[$room_id] = $roomWaitings;
|
||||
// 保存
|
||||
$this->nspData('waitings', $waitings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 删除对应房间中排队的顾客,如果不存在忽略
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $session_id
|
||||
* @return void
|
||||
*/
|
||||
public function nspWaitingDel($room_id, $session_id)
|
||||
{
|
||||
// 所有 waitings
|
||||
$waitings = $this->nspData('waitings');
|
||||
$waitings = $waitings ?: [];
|
||||
|
||||
// 当前房间的 waitings
|
||||
$roomWaitings = $waitings[$room_id] ?? [];
|
||||
|
||||
$key = array_search($session_id, $roomWaitings);
|
||||
if ($key !== false) {
|
||||
// 移除
|
||||
unset($roomWaitings[$key]);
|
||||
|
||||
// 重新赋值
|
||||
$waitings[$room_id] = array_values($roomWaitings);
|
||||
|
||||
$this->nspData('waitings', $waitings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取在房间中的排名
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $session_id
|
||||
* @return integer
|
||||
*/
|
||||
public function nspWaitingRank($room_id, $session_id)
|
||||
{
|
||||
// 所有 waitings
|
||||
$waitings = $this->nspData('waitings');
|
||||
$waitings = $waitings ?: [];
|
||||
|
||||
// 当前房间的 waitings
|
||||
$roomWaitings = $waitings[$room_id] ?? [];
|
||||
|
||||
// 获取 session_id 的下标,就是当前顾客前面还有多少位 顾客
|
||||
$key = array_search($session_id, $roomWaitings);
|
||||
|
||||
return $key !== false ? $key : 0;
|
||||
}
|
||||
}
|
||||
158
addons/shopro/library/chat/traits/Session.php
Normal file
158
addons/shopro/library/chat/traits/Session.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\traits;
|
||||
|
||||
/**
|
||||
* 客服 session 工具类
|
||||
*/
|
||||
trait Session
|
||||
{
|
||||
|
||||
/**
|
||||
* 获取存储的数据,当前 socket
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function session($name = null, $value = '')
|
||||
{
|
||||
$data = $this->socket->session ?? [];
|
||||
|
||||
if (is_null($name)) {
|
||||
// 获取全部数据
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ('' === $value) {
|
||||
// 获取缓存
|
||||
return 0 === strpos($name, '?') ? isset($data[$name]) : ($data[$name] ?? null);
|
||||
} elseif (is_null($value)) {
|
||||
// 删除缓存
|
||||
unset($data[$name]);
|
||||
$this->socket->session = $data;
|
||||
}
|
||||
|
||||
// 存数据
|
||||
$data[$name] = $value;
|
||||
$this->socket->session = $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 client_id 获取指定 client_id 的 session 数据
|
||||
*
|
||||
* @param string $client_id 要获取 session 的 client_id
|
||||
* @param string $name 要获取 session 中的特定 name
|
||||
* @return string|array
|
||||
*/
|
||||
public function getSession($client_id, $name = null)
|
||||
{
|
||||
$client = $this->nsp->sockets[$client_id] ?? null;
|
||||
|
||||
$session = is_null($client) ? null : (isset($client->session) && $client->session ? $client->session : []);
|
||||
|
||||
return $name ? ($session[$name] ?? null) : $session;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调用 getSession,别名
|
||||
*
|
||||
* @param string $client_id 要获取 session 的 client_id
|
||||
* @param string $name 要获取 session 中的特定 name
|
||||
* @return string|array
|
||||
*/
|
||||
public function getSessionByClientId($client_id, $name = null)
|
||||
{
|
||||
return $this->getSession($client_id, $name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 client_ids 获取指定 client_ids 的 session 数据集合
|
||||
*
|
||||
* @param string $client_id 要获取 session 的 client_id
|
||||
* @param string $name 要获取 session 中的特定 name
|
||||
* @return array
|
||||
*/
|
||||
public function getSessionByClientIds($clientIds, $name = null)
|
||||
{
|
||||
$customerServices = [];
|
||||
foreach ($clientIds as $client_id) {
|
||||
$currentCustomerService = $this->getSessionByClientId($client_id, $name);
|
||||
if ($currentCustomerService) {
|
||||
$customerServices[] = $currentCustomerService;
|
||||
}
|
||||
}
|
||||
|
||||
return $customerServices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通过 client_id 更新指定 client_id 的 session 数据
|
||||
*
|
||||
* @param string $client_id 要获取 session 的 client_id
|
||||
* @param array $data 要更新 session 中的特定 name
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateSession($client_id, $data = [])
|
||||
{
|
||||
$client = $this->nsp->sockets[$client_id] ?? null;
|
||||
|
||||
if (!$client) {
|
||||
// client 没有,可能下线了,直接返回
|
||||
return false;
|
||||
}
|
||||
|
||||
// session 不存在,初始化
|
||||
if (!isset($client->session)) {
|
||||
$this->nsp->sockets[$client_id]->session = [];
|
||||
}
|
||||
|
||||
// 更新对应的键
|
||||
foreach ($data as $key => $value) {
|
||||
$current = $this->nsp->sockets[$client_id]->session[$key] ?? null;
|
||||
if (is_array($value) && $current) {
|
||||
// 合并数组,比如修改 session 中 customer_service 客服信息 中的 status 在线状态(注意,如果value 为空,则覆盖,如果有值,则合并)
|
||||
$this->nsp->sockets[$client_id]->session[$key] = $value ? array_merge($current, $value) : $value;
|
||||
} else {
|
||||
$this->nsp->sockets[$client_id]->session[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调用 updateSession,别名
|
||||
*
|
||||
* @param string $client_id 要获取 session 的 client_id
|
||||
* @param array $data 要更新 session 中的特定 name
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateSessionByClientId($client_id, $data = [])
|
||||
{
|
||||
return $this->updateSession($client_id, $data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 client_id 更新指定 client_id 的 session 数据集合
|
||||
*
|
||||
* @param string $client_id 要获取 session 的 client_id
|
||||
* @param string $name 要获取 session 中的特定 name
|
||||
* @return array
|
||||
*/
|
||||
public function updateSessionByClientIds($clientIds, $data = [])
|
||||
{
|
||||
foreach ($clientIds as $client_id) {
|
||||
$this->updateSessionByClientId($client_id, $data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
444
addons/shopro/library/chat/traits/sender/SenderFunc.php
Normal file
444
addons/shopro/library/chat/traits/sender/SenderFunc.php
Normal file
@@ -0,0 +1,444 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\chat\traits\sender;
|
||||
|
||||
use addons\shopro\library\chat\traits\Helper;
|
||||
use addons\shopro\library\chat\traits\Session;
|
||||
use addons\shopro\library\chat\traits\NspData;
|
||||
|
||||
/**
|
||||
* 绑定 uid
|
||||
*/
|
||||
trait SenderFunc
|
||||
{
|
||||
use Session;
|
||||
|
||||
use Helper;
|
||||
|
||||
use NspData;
|
||||
|
||||
/**
|
||||
* 用户自己触发:发送给自己,登录成功
|
||||
*
|
||||
* @param \Closure $callback
|
||||
* @return void
|
||||
*/
|
||||
public function authSuccess($callback)
|
||||
{
|
||||
// 通过回调发送给自己
|
||||
$this->successSocket($callback, '连接成功', [
|
||||
'session_id' => $this->session('session_id'),
|
||||
'chat_user' => $this->session('chat_user') ?: null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 用户自己触发:通知所有房间中的客服,用户上线了
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerOnline()
|
||||
{
|
||||
$session_id = $this->session('session_id');
|
||||
$room_id = $this->session('room_id');
|
||||
$chatUser = $this->session('chat_user');
|
||||
$chatUser = $this->getter()->chatUsersFormat($room_id, [$chatUser], ['first' => true]);
|
||||
|
||||
$this->successRoom('customer_online', '顾客上线', [
|
||||
'session_id' => $session_id,
|
||||
'chat_user' => $chatUser,
|
||||
], $this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户自己触发:通知所有房间中的客服,用户下线了
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerOffline()
|
||||
{
|
||||
$session_id = $this->session('session_id');
|
||||
$room_id = $this->session('room_id');
|
||||
$chatUser = $this->session('chat_user');
|
||||
$chatUser = $this->getter()->chatUsersFormat($room_id, [$chatUser], ['first' => true]);
|
||||
|
||||
$this->successRoom('customer_offline', '顾客下线', [
|
||||
'session_id' => $session_id,
|
||||
'chat_user' => $chatUser,
|
||||
], $this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户自己触发:通知用户的客户端,排队中,通知客服,有新的等待中用户
|
||||
*
|
||||
* @param array $waiting 等待中的顾客
|
||||
* @return void
|
||||
*/
|
||||
public function waiting()
|
||||
{
|
||||
$session_id = $this->session('session_id');
|
||||
$room_id = $this->session('room_id');
|
||||
|
||||
// 通知客服更新 等待中列表,返回整个等待的列表
|
||||
$this->successRoom('customer_waiting', '顾客等待', [
|
||||
'waitings' => $this->getter()->getCustomersFormatWaiting($room_id),
|
||||
], $this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
|
||||
// 通知用户前面排队人数
|
||||
$this->waitingQueue($room_id, $session_id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户自己触发,服务器定时触发:通知用户的客户端,当前有客服,但是需要排队 (传参是因为别的地方也需要调用)
|
||||
*
|
||||
* @param string $room_id
|
||||
* @param string $session_id
|
||||
* @return void
|
||||
*/
|
||||
public function waitingQueue($room_id, $session_id)
|
||||
{
|
||||
$rank = $this->nspWaitingRank($room_id, $session_id);
|
||||
|
||||
$title = $rank > 0 ? '当前还有 ' . $rank . ' 位顾客,请耐心等待' : '客服马上为您服务,请稍等';
|
||||
|
||||
// 通知用户等待接入
|
||||
$this->successUId('waiting_queue', '排队等待', [
|
||||
'title' => $title,
|
||||
], ['id' => $session_id, 'type' => 'customer']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 当有变动,批量通知排队等待排名
|
||||
*
|
||||
* @param string|null $room_id
|
||||
* @return void
|
||||
*/
|
||||
public function allWaitingQueue($room_id = null)
|
||||
{
|
||||
$waitings = $this->nspGetWaitings($room_id);
|
||||
|
||||
if ($room_id) {
|
||||
// 只处理该房间的
|
||||
if ($this->getter->driver('socket')->hasCustomerServiceByRoomId($room_id)) {
|
||||
foreach ($waitings as $key => $session_id) {
|
||||
$this->waitingQueue($room_id, $session_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 定时器调用, 处理所有房间的
|
||||
foreach ($waitings as $current_room_id => $roomWaitings) {
|
||||
// 判断是否有客服在线
|
||||
if ($this->getter->driver('socket')->hasCustomerServiceByRoomId($current_room_id)) {
|
||||
foreach ($roomWaitings as $key => $session_id) {
|
||||
$this->waitingQueue($current_room_id, $session_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户自己触发:通知用户的客户端,当前没有客服在线
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function noCustomerService()
|
||||
{
|
||||
$session_id = $this->session('session_id');
|
||||
|
||||
$this->successUId('no_customer_service', '暂无客服在线', [
|
||||
'message' => [
|
||||
'message_type' => 'system',
|
||||
'message' => '当前客服不在线',
|
||||
'createtime' => time()
|
||||
],
|
||||
], ['id' => $session_id, 'type' => 'customer']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 给客服发消息
|
||||
*
|
||||
* @param array $message 消息原始内容
|
||||
* @param array $sender 发送者信息
|
||||
* @param integer $customer_service_id 客服 id
|
||||
* @return void
|
||||
*/
|
||||
public function messageToCustomerService($message, $sender, $customer_service_id)
|
||||
{
|
||||
// 给客服发送消息
|
||||
$this->messageUId('message', '收到消息', [
|
||||
'message' => $message,
|
||||
'sender' => $sender
|
||||
], ['id' => $customer_service_id, 'type' => 'customer_service']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 给顾客发消息
|
||||
*
|
||||
* @param array $message 消息原始内容
|
||||
* @param array $sender 发送者信息
|
||||
* @param integer $session_id 顾客 session_id
|
||||
* @return void
|
||||
*/
|
||||
public function messageToCustomer($message, $sender, $session_id)
|
||||
{
|
||||
// 给用户发送消息
|
||||
$this->messageUId('message', '收到消息', [
|
||||
'message' => $message,
|
||||
'sender' => $sender
|
||||
], ['id' => $session_id, 'type' => 'customer']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 同时通知客服和顾客
|
||||
*
|
||||
* @param array $question 要回答的问题
|
||||
* @param string $session_id 用户表示
|
||||
* @param string $customer_service_id 客服
|
||||
* @return void
|
||||
*/
|
||||
public function messageToBoth($question, $session_id, $customer_service_id)
|
||||
{
|
||||
$customerService = $this->session('customer_service');
|
||||
$chatUser = $this->session('chat_user');
|
||||
|
||||
$message = [
|
||||
'message_type' => 'text',
|
||||
'message' => $question['content'],
|
||||
];
|
||||
$sender = [
|
||||
'sender_identify' => 'customer_service',
|
||||
'customer_service' => $customerService,
|
||||
];
|
||||
// 发给用户
|
||||
$this->messageUId('message', '收到消息', [
|
||||
'message' => $message,
|
||||
'sender' => $sender
|
||||
], ['id' => $session_id, 'type' => 'customer']);
|
||||
|
||||
// 发给客服
|
||||
if ($customer_service_id) {
|
||||
// 有客服,发给客服
|
||||
$message['sender_identify'] = 'customer_service';
|
||||
$message['sender_id'] = $customerService['id'];
|
||||
$message['sender'] = $customerService;
|
||||
$message['createtime'] = time();
|
||||
$sender['session_id'] = $session_id;
|
||||
$this->successUId('message', '收到消息', [
|
||||
'message' => $message,
|
||||
'sender' => $sender
|
||||
], ['id' => $customer_service_id, 'type' => 'customer_service']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通知连接的用户(在当前客服服务的房间里面的用户),客服上线了
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceOnline()
|
||||
{
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 给客服发送消息
|
||||
$this->successRoom('customer_service_online', '客服 ' . $customerService['name'] . ' 上线', [
|
||||
'customer_service' => $customerService,
|
||||
], $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通知连接的用户(在当前客服服务的房间里面的用户),客服忙碌
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceBusy()
|
||||
{
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
// 给客服发送消息
|
||||
$this->successRoom('customer_service_busy', '客服 ' . $customerService['name'] . ' 忙碌', [
|
||||
'customer_service' => $customerService,
|
||||
], $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知连接的用户(在当前客服服务的房间里面的用户),客服下线了
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceOffline()
|
||||
{
|
||||
$room_id = $this->session('room_id');
|
||||
$customerService = $this->session('customer_service');
|
||||
|
||||
$this->successRoom('customer_service_offline', '客服 ' . $customerService['name'] . ' 离线', [
|
||||
'customer_service' => $customerService,
|
||||
], $this->getRoomName('customer_service_room_user', ['room_id' => $room_id, 'customer_service_id' => $customerService['id']]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通知当前房间的在线客服,更新当前在线客服列表
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceUpdate()
|
||||
{
|
||||
$room_id = $this->session('room_id');
|
||||
|
||||
// 给客服发送消息
|
||||
$customerServices = $this->getter('socket')->getCustomerServicesByRoomId($room_id);
|
||||
$this->successRoom('customer_service_update', '更新客服列表', [
|
||||
'customer_services' => $customerServices,
|
||||
], $this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 服务结束,通知顾客客服断开连接
|
||||
*
|
||||
* @param string $session_id
|
||||
* @return void
|
||||
*/
|
||||
public function customerServiceBreak($session_id)
|
||||
{
|
||||
$this->successUId('customer_service_break', '客服断开', [
|
||||
'message' => [
|
||||
'message_type' => 'system',
|
||||
'message' => '服务已结束',
|
||||
'createtime' => time()
|
||||
],
|
||||
], ['id' => $session_id, 'type' => 'customer']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服转接
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $session_id 用户 UId
|
||||
* @param array $customerService 老客服
|
||||
* @param array $newCustomerService 新客服
|
||||
* @return void
|
||||
*/
|
||||
public function customerTransfer($room_id, $session_id, $customerService, $newCustomerService)
|
||||
{
|
||||
// 通知用户客服被转接
|
||||
$this->customerAccessedToCustomer($session_id, $customerService, $newCustomerService);
|
||||
|
||||
// 通知所有客服用户被接入
|
||||
$this->customerAccessedToCustomerServices($room_id, $session_id, $newCustomerService);
|
||||
|
||||
// 通知新的客服,新用户接入
|
||||
$this->customerAccessedToCustomerService($room_id, $session_id, $newCustomerService);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 客服接入
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $session_id 用户 UId
|
||||
* @param array $customerService 老客服
|
||||
* @return void
|
||||
*/
|
||||
public function customerAccessed($room_id, $session_id, $customerService)
|
||||
{
|
||||
// 通知用户客服接入
|
||||
$this->customerAccessedToCustomer($session_id, $customerService);
|
||||
|
||||
// 通知所有客服用户被接入
|
||||
$this->customerAccessedToCustomerServices($room_id, $session_id, $customerService);
|
||||
|
||||
// 通知新的客服,新用户接入
|
||||
$this->customerAccessedToCustomerService($room_id, $session_id, $customerService);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通知顾客 客服接入
|
||||
*
|
||||
* @param string $session_id 用户 UId
|
||||
* @param array $customerService 老客服
|
||||
* @param array $newCustomerService 新客服
|
||||
* @return void
|
||||
*/
|
||||
private function customerAccessedToCustomer($session_id, $customerService, $newCustomerService = null)
|
||||
{
|
||||
// 通知当前用户的所有客户端,客服接入
|
||||
$message = '您好,客服 ' . $customerService['name'] . " 为您服务";
|
||||
if ($newCustomerService) {
|
||||
$message = '您好,您的客服已由 ' . $customerService['name'] . " 切换为 " . $newCustomerService['name'];
|
||||
}
|
||||
|
||||
$this->successUId('customer_service_access', '客服接入', [
|
||||
'message' => [
|
||||
'message_type' => 'system',
|
||||
'message' => $message,
|
||||
'createtime' => time()
|
||||
],
|
||||
'customer_service' => $newCustomerService ? : $customerService
|
||||
], ['id' => $session_id, 'type' => 'customer']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通知所有客服,用户被接入
|
||||
*
|
||||
* @param string $room_id 房间号
|
||||
* @param string $session_id 用户 UId
|
||||
* @param array $customerService 老客服
|
||||
* @return void
|
||||
*/
|
||||
private function customerAccessedToCustomerServices($room_id, $session_id, $customerService)
|
||||
{
|
||||
// 通知所有客服,用户被接入
|
||||
$this->successRoom('customer_accessed', '顾客被接入', [
|
||||
'session_id' => $session_id,
|
||||
'chat_user' => $this->getter('db')->getChatUserBySessionId($session_id),
|
||||
'customer_service' => $customerService,
|
||||
], $this->getRoomName('customer_service_room', ['room_id' => $room_id]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通知被接入客服,新用户接入
|
||||
*
|
||||
* @param [type] $session_id
|
||||
* @param [type] $customerService
|
||||
* @return void
|
||||
*/
|
||||
private function customerAccessedToCustomerService($room_id, $session_id, $customerService)
|
||||
{
|
||||
// 获取chatUser
|
||||
$chatUser = $this->getter('db')->getChatUserBySessionId($session_id);
|
||||
// 格式化chatUser
|
||||
$chatUser = $this->getter()->chatUsersFormat($room_id, [$chatUser], ['first' => true]);
|
||||
|
||||
// 通知新的客服,新用户接入
|
||||
$this->successUId('customer_access', '新顾客接入', [
|
||||
'session_id' => $session_id,
|
||||
'chat_user' => $chatUser,
|
||||
], ['id' => $customerService['id'], 'type' => 'customer_service']);
|
||||
}
|
||||
}
|
||||
35
addons/shopro/library/easywechatPlus/EasywechatPlus.php
Normal file
35
addons/shopro/library/easywechatPlus/EasywechatPlus.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\easywechatPlus;
|
||||
|
||||
|
||||
/**
|
||||
* 补充 easywechat
|
||||
*/
|
||||
class EasywechatPlus
|
||||
{
|
||||
|
||||
protected $app = null;
|
||||
|
||||
public function __construct($app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
|
||||
// 返回实例
|
||||
public function getApp()
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
|
||||
//获取accessToken
|
||||
protected function getAccessToken()
|
||||
{
|
||||
$accessToken = $this->app->access_token;
|
||||
$token = $accessToken->getToken(); // token 数组 token['access_token'] 字符串
|
||||
//$token = $accessToken->getToken(true); // 强制重新从微信服务器获取 token.
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
317
addons/shopro/library/easywechatPlus/WechatMiniProgramShop.php
Normal file
317
addons/shopro/library/easywechatPlus/WechatMiniProgramShop.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\easywechatPlus;
|
||||
|
||||
use addons\shopro\exception\ShoproException;
|
||||
use addons\shopro\service\order\shippingInfo\OrderShippingInfo;
|
||||
use addons\shopro\service\order\shippingInfo\TradeOrderShippingInfo;
|
||||
use app\admin\model\shopro\data\WechatExpress;
|
||||
|
||||
/**
|
||||
* 补充 小程序购物订单
|
||||
*/
|
||||
class WechatMiniProgramShop extends EasywechatPlus
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* 上送订单信息
|
||||
*
|
||||
* @param object|array $order
|
||||
* @return void
|
||||
*/
|
||||
public function uploadShippingInfos($order, $express = null, $type = 'send')
|
||||
{
|
||||
try {
|
||||
$orderShippingInfo = new OrderShippingInfo($order);
|
||||
|
||||
if ($type == 'change') {
|
||||
$uploadParams = $orderShippingInfo->getChangeShippingParams($express);
|
||||
} else {
|
||||
$uploadParams = $orderShippingInfo->getShippingParams();
|
||||
}
|
||||
|
||||
// 设置消息跳转地址
|
||||
$this->setMessageJumpPath();
|
||||
|
||||
$length = count($uploadParams);
|
||||
foreach ($uploadParams as $key => $params) {
|
||||
$params['delivery_mode'] = 1;
|
||||
$params['is_all_delivered'] = true;
|
||||
|
||||
if ($params['logistics_type'] == 1 && count($params['shipping_list']) > 1) {
|
||||
// 快递物流,并且
|
||||
$params['delivery_mode'] = 2;
|
||||
}
|
||||
|
||||
if ($length > 1) {
|
||||
if ($key == ($length - 1)) {
|
||||
// 最后一条
|
||||
$params['is_all_delivered'] = true; // 发货完成
|
||||
} else {
|
||||
$params['is_all_delivered'] = false; // 发货未完成
|
||||
}
|
||||
}
|
||||
|
||||
$params['upload_time'] = date(DATE_RFC3339);
|
||||
|
||||
\think\Log::error('发货信息录入' . json_encode($params));
|
||||
$result = $this->uploadShippingInfo($params);
|
||||
|
||||
if ($result['errcode'] != 0) {
|
||||
throw new ShoproException('获取失败: errcode:' . $result['errcode'] . '; errmsg:' . $result['errmsg']);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
format_log_error($e, 'upload_shipping_info', '发货信息录入错误');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* trade 订单上送订单信息
|
||||
*
|
||||
* @param object|array $order
|
||||
* @return void
|
||||
*/
|
||||
public function tradeUploadShippingInfos($order)
|
||||
{
|
||||
try {
|
||||
$orderShippingInfo = new TradeOrderShippingInfo($order);
|
||||
|
||||
$uploadParams = $orderShippingInfo->getShippingParams();
|
||||
|
||||
// 将确认收货跳转地址设置为空,trade 商城系统不需要确认收货
|
||||
$this->setMessageJumpPath(true, '');
|
||||
|
||||
$length = count($uploadParams);
|
||||
foreach ($uploadParams as $key => $params) {
|
||||
$params['delivery_mode'] = 1;
|
||||
$params['is_all_delivered'] = true;
|
||||
$params['upload_time'] = date(DATE_RFC3339);
|
||||
|
||||
\think\Log::error('发货信息录入' . json_encode($params));
|
||||
$result = $this->uploadShippingInfo($params);
|
||||
|
||||
if ($result['errcode'] != 0) {
|
||||
throw new ShoproException('获取失败: errcode:' . $result['errcode'] . '; errmsg:' . $result['errmsg']);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
format_log_error($e, 'trade_upload_shipping_info', '发货信息录入错误');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将发货信息提交给微信
|
||||
*
|
||||
* @param array $params 上送参数
|
||||
* @return void
|
||||
*/
|
||||
private function uploadShippingInfo($params)
|
||||
{
|
||||
$access_token = $this->getAccessToken();
|
||||
|
||||
$add_template_url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info";
|
||||
$result = \addons\shopro\facade\HttpClient::request('post', $add_template_url, [
|
||||
'body' => json_encode($params, JSON_UNESCAPED_UNICODE),
|
||||
'query' => ["access_token" => $access_token['access_token']],
|
||||
'headers' => ['Content-Type' => 'application/json']
|
||||
]);
|
||||
|
||||
$result = $result->getBody()->getContents();
|
||||
return json_decode($result, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检测并且设置微信消息跳转地址
|
||||
*
|
||||
* @param boolean $exception
|
||||
* @param boolean $is_force
|
||||
* @return boolean
|
||||
*/
|
||||
public function checkAndSetMessageJumpPath($exception = false, $is_force = false)
|
||||
{
|
||||
try {
|
||||
// 查询是否又微信发货管理权限
|
||||
if ($this->isTradeManaged($is_force)) {
|
||||
// 有权限,设置消息跳转地址
|
||||
$this->setMessageJumpPath($is_force);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
format_log_error($e, 'checkAndSetMessageJumpPath', '自动设置微信小程序发货信息管理消息跳转路径失败');
|
||||
if ($exception) {
|
||||
// 抛出异常
|
||||
throw new ShoproException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询是否有微信发货信息管理权限 (48001 时当没有权限处理,不抛出异常)
|
||||
*
|
||||
* @param boolean $is_force
|
||||
* @return boolean
|
||||
*/
|
||||
public function isTradeManaged($is_force = false)
|
||||
{
|
||||
$key = 'wechat:is_trade_managed';
|
||||
if (!$is_force && redis_cache('?' . $key)) {
|
||||
return redis_cache($key); // 直接返回是否有权限
|
||||
}
|
||||
|
||||
$access_token = $this->getAccessToken();
|
||||
$add_template_url = "https://api.weixin.qq.com/wxa/sec/order/is_trade_managed";
|
||||
|
||||
$mini_appid = sheep_config('shop.platform.WechatMiniProgram.app_id');
|
||||
if (!$mini_appid) {
|
||||
// 没有配置微信小程序参数
|
||||
throw new ShoproException('微信小程序发货管理查询失败,没有配置微信小程序');
|
||||
}
|
||||
|
||||
$params = [
|
||||
'appid' => $mini_appid
|
||||
];
|
||||
$result = \addons\shopro\facade\HttpClient::request('post', $add_template_url, [
|
||||
'body' => json_encode($params, JSON_UNESCAPED_UNICODE),
|
||||
'query' => ["access_token" => $access_token['access_token']],
|
||||
'headers' => ['Content-Type' => 'application/json']
|
||||
]);
|
||||
|
||||
$result = $result->getBody()->getContents();
|
||||
$result = json_decode($result, true);
|
||||
|
||||
if ($result['errcode'] != 0 && $result['errcode'] != '48001') {
|
||||
// 48001 时不抛出异常,当没有权限处理
|
||||
throw new ShoproException('查询是否有微信发货信息管理权限失败: errcode:' . $result['errcode'] . '; errmsg:' . $result['errmsg']);
|
||||
}
|
||||
|
||||
$is_trade_managed = isset($result['is_trade_managed']) ? intval($result['is_trade_managed']) : 0;
|
||||
|
||||
redis_cache($key, $is_trade_managed, 7200); // 缓存结果,两小时
|
||||
|
||||
return $is_trade_managed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置微信消息跳转路径
|
||||
*
|
||||
* @param boolean $is_force
|
||||
* @return void
|
||||
*/
|
||||
public function setMessageJumpPath($is_force = false, $path = 'pages/order/detail?comein_type=wechat')
|
||||
{
|
||||
if (!$is_force && redis_cache('?wechat:set_message_jump_path')) {
|
||||
// 已经设置过了,无需再次设置
|
||||
return true;
|
||||
}
|
||||
|
||||
$access_token = $this->getAccessToken();
|
||||
$add_template_url = "https://api.weixin.qq.com/wxa/sec/order/set_msg_jump_path";
|
||||
|
||||
$params = [
|
||||
'path' => $path
|
||||
];
|
||||
$result = \addons\shopro\facade\HttpClient::request('post', $add_template_url, [
|
||||
'body' => json_encode($params, JSON_UNESCAPED_UNICODE),
|
||||
'query' => ["access_token" => $access_token['access_token']],
|
||||
'headers' => ['Content-Type' => 'application/json']
|
||||
]);
|
||||
|
||||
$result = $result->getBody()->getContents();
|
||||
$result = json_decode($result, true);
|
||||
|
||||
if ($result['errcode'] != 0) {
|
||||
throw new ShoproException('设置微信发货消息跳转地址失败: errcode:' . $result['errcode'] . '; errmsg:' . $result['errmsg']);
|
||||
}
|
||||
|
||||
if ($is_force) {
|
||||
// 充值订单发货时,清掉缓存
|
||||
redis_cache('wechat:set_message_jump_path', null); // 清除缓存
|
||||
} else {
|
||||
redis_cache('wechat:set_message_jump_path', time()); // 永久有效
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取微信delivery数据
|
||||
*
|
||||
* @param boolean $is_force
|
||||
* @return void
|
||||
*/
|
||||
public function getDelivery($is_force = false)
|
||||
{
|
||||
if (!$is_force && redis_cache('?wechat:get_delivery_list')) {
|
||||
// 已经设置过了,无需再次设置
|
||||
return true;
|
||||
}
|
||||
|
||||
$access_token = $this->getAccessToken();
|
||||
$get_delivery_url = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/get_delivery_list";
|
||||
|
||||
$result = \addons\shopro\facade\HttpClient::request('post', $get_delivery_url, [
|
||||
'body' => '{}',
|
||||
'query' => ["access_token" => $access_token['access_token']],
|
||||
'headers' => ['Content-Type' => 'application/json']
|
||||
]);
|
||||
|
||||
$result = $result->getBody()->getContents();
|
||||
$result = json_decode($result, true);
|
||||
|
||||
if ($result['errcode'] != 0) {
|
||||
throw new ShoproException('获取微信 delivery 列表失败: errcode:' . $result['errcode'] . '; errmsg:' . $result['errmsg']);
|
||||
}
|
||||
|
||||
// 存库
|
||||
$datas = $result['delivery_list'];
|
||||
foreach ($datas as $data) {
|
||||
$wechatExpress = WechatExpress::where('code', $data['delivery_id'])->find();
|
||||
$current = [
|
||||
'name' => $data['delivery_name'] ?? '',
|
||||
'code' => $data['delivery_id'] ?? '',
|
||||
];
|
||||
|
||||
if (!$wechatExpress) {
|
||||
$wechatExpress = new WechatExpress();
|
||||
}
|
||||
|
||||
$wechatExpress->save($current);
|
||||
}
|
||||
|
||||
redis_cache('wechat:get_delivery_list', time()); // 永久有效
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 方法转发到 easywechat
|
||||
*
|
||||
* @param string $funcname
|
||||
* @param array $arguments
|
||||
* @return void
|
||||
*/
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
// if ($funcname == 'deletePrivateTemplate') {
|
||||
// return $this->app->template_message->{$funcname}(...$arguments);
|
||||
// }
|
||||
|
||||
return $this->app->{$funcname}(...$arguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\easywechatPlus;
|
||||
|
||||
/**
|
||||
* 补充 公众号行业模板
|
||||
*/
|
||||
class WechatOfficialTemplate extends EasywechatPlus
|
||||
{
|
||||
|
||||
/**
|
||||
* 添加公众号模板
|
||||
*
|
||||
* @param string $shortId 模板 id
|
||||
* @param array $keywordList 模板关键字
|
||||
* @return void
|
||||
*/
|
||||
public function addTemplate($shortId, $keywordList)
|
||||
{
|
||||
$params = ['template_id_short' => $shortId];
|
||||
|
||||
if ($keywordList) {
|
||||
$params['keyword_name_list'] = $keywordList;
|
||||
}
|
||||
|
||||
$access_token = $this->getAccessToken();
|
||||
|
||||
$add_template_url = "https://api.weixin.qq.com/cgi-bin/template/api_add_template";
|
||||
$result = \addons\shopro\facade\HttpClient::request('post', $add_template_url, [
|
||||
'body' => json_encode($params, JSON_UNESCAPED_UNICODE),
|
||||
'query' => ["access_token" => $access_token['access_token']],
|
||||
'headers' => ['Content-Type' => 'application/json']
|
||||
]);
|
||||
|
||||
$result = $result->getBody()->getContents();
|
||||
return json_decode($result, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 方法转发到 easywechat
|
||||
*
|
||||
* @param string $funcname
|
||||
* @param array $arguments
|
||||
* @return void
|
||||
*/
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
if ($funcname == 'deletePrivateTemplate') {
|
||||
return $this->app->template_message->{$funcname}(...$arguments);
|
||||
}
|
||||
|
||||
return $this->app->{$funcname}(...$arguments);
|
||||
}
|
||||
}
|
||||
117
addons/shopro/library/express/Express.php
Normal file
117
addons/shopro/library/express/Express.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\express;
|
||||
|
||||
use think\exception\HttpResponseException;
|
||||
use app\admin\model\shopro\order\Express as ExpressModel;
|
||||
|
||||
class Express
|
||||
{
|
||||
|
||||
/**
|
||||
* 快递驱动
|
||||
*/
|
||||
protected $driver = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->driver = sheep_config('shop.dispatch.driver');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 提供器
|
||||
*
|
||||
* @param string $type
|
||||
* @return Base
|
||||
*/
|
||||
public function provider($driver = null)
|
||||
{
|
||||
$driver = $driver ?: $this->getDefaultDriver();
|
||||
$class = "\\addons\\shopro\\library\\express\\provider\\" . \think\helper\Str::studly($driver);
|
||||
if (class_exists($class)) {
|
||||
return new $class($this);
|
||||
}
|
||||
|
||||
error_stop('物流平台类型不支持');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新订单的所有包裹
|
||||
*
|
||||
* @param mixed $orderExpress
|
||||
* @return void
|
||||
*/
|
||||
public function updateOrderExpress($orderExpress = 0)
|
||||
{
|
||||
try {
|
||||
if ($this->driver == 'thinkapi') {
|
||||
// thinkapi 才需要查询
|
||||
if (is_numeric($orderExpress)) {
|
||||
$orderExpresses = ExpressModel::where('order_id', $orderExpress)->select();
|
||||
}
|
||||
foreach ($orderExpresses as $key => $orderExpress) {
|
||||
$this->updateExpress($orderExpress);
|
||||
}
|
||||
}
|
||||
} catch (HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
format_log_error($e, 'updateOrderExpress.HttpResponseException', $message);
|
||||
} catch(\Exception $e) {
|
||||
format_log_error($e, 'updateOrderExpress.HttpResponseException', '获取物流信息错误');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新单个包裹
|
||||
*
|
||||
* @param \think\Model $orderExpress
|
||||
* @return void
|
||||
*/
|
||||
public function updateExpress($orderExpress)
|
||||
{
|
||||
if ($this->driver == 'thinkapi' && $orderExpress->status != 'signfor') {
|
||||
// thinkapi 并且是未签收的包裹才更新
|
||||
$key = 'express:' . $orderExpress->id . ':code:' . $orderExpress->express_no; // 包裹 id 拼上 运单号
|
||||
if (cache('?'.$key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 查询物流信息,并且更新 express_log
|
||||
$this->provider()->search([
|
||||
'order_id' => $orderExpress['order_id'],
|
||||
'express_code' => $orderExpress['express_code'],
|
||||
'express_no' => $orderExpress['express_no']
|
||||
], $orderExpress);
|
||||
|
||||
// 缓存 300 秒
|
||||
cache($key, time(), 300);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 默认快递物流驱动
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getDefaultDriver()
|
||||
{
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
return $this->provider()->{$funcname}(...$arguments);
|
||||
}
|
||||
|
||||
}
|
||||
196
addons/shopro/library/express/adapter/Kdniao.php
Normal file
196
addons/shopro/library/express/adapter/Kdniao.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\express\adapter;
|
||||
|
||||
use fast\Http;
|
||||
|
||||
class Kdniao
|
||||
{
|
||||
// 查询接口
|
||||
const REQURL = "https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx";
|
||||
// 订阅接口
|
||||
const SUBURL = "https://api.kdniao.com/api/dist";
|
||||
// 电子面单下单接口
|
||||
const EORDER = "https://api.kdniao.com/api/EOrderService";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 发货人信息
|
||||
*/
|
||||
protected $sender = [];
|
||||
|
||||
/**
|
||||
* 快递鸟配置参数
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = sheep_config('shop.dispatch.kdniao');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 物流查询
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function search($data)
|
||||
{
|
||||
$requestParams = $this->getRequestParams($data);
|
||||
$requestData = $this->getRequestData($requestParams);
|
||||
$requestData['RequestType'] = $this->config['type'] == 'free' ? '1002' : '8001';
|
||||
|
||||
$result = Http::post(self::REQURL, $requestData);
|
||||
|
||||
$result = $this->getResponse($result, '没有物流信息');
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 物流订阅
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function subscribe($data)
|
||||
{
|
||||
$requestParams = $this->getRequestParams($data);
|
||||
$requestData = $this->getRequestData($requestParams);
|
||||
$requestData['RequestType'] = $this->config['type'] == 'free' ? '1008' : '8008';
|
||||
|
||||
$result = Http::post(self::SUBURL, $requestData);
|
||||
|
||||
$result = $this->getResponse($result, '订阅失败');
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function cancel($data)
|
||||
{
|
||||
$requestData = $data;
|
||||
|
||||
$requestData = $this->getRequestData($data);
|
||||
$requestData['RequestType'] = '1147';
|
||||
|
||||
$result = Http::post(self::EORDER, $requestData);
|
||||
|
||||
$result = $this->getResponse($result, '电子面单取消失败');
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 电子面单
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function eOrder($data)
|
||||
{
|
||||
$requestData = $this->getRequestData($data);
|
||||
$requestData['RequestType'] = '1007';
|
||||
|
||||
$result = Http::post(self::EORDER, $requestData);
|
||||
|
||||
$result = $this->getResponse($result, '电子面单下单失败');
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 快递鸟物流推送结果处理
|
||||
*
|
||||
* @param boolean $success
|
||||
* @param string $reason
|
||||
* @return string
|
||||
*/
|
||||
public function pushResult($success, $reason)
|
||||
{
|
||||
$result = [
|
||||
"EBusinessID" => $this->config['ebusiness_id'],
|
||||
"UpdateTime" => date('Y-m-d H:i:s'),
|
||||
"Success" => $success,
|
||||
'Reason' => $reason
|
||||
];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 组装请求数据
|
||||
*
|
||||
* @param array $requestParams
|
||||
* @return array
|
||||
*/
|
||||
private function getRequestData($requestParams)
|
||||
{
|
||||
$requestParams = is_array($requestParams) ? json_encode($requestParams, JSON_UNESCAPED_UNICODE) : $requestParams;
|
||||
|
||||
$requestData = [
|
||||
'EBusinessID' => $this->config['ebusiness_id'],
|
||||
'RequestData' => urlencode($requestParams),
|
||||
'DataType' => '2',
|
||||
];
|
||||
$requestData['DataSign'] = $this->encrypt($requestParams, $this->config['app_key']);
|
||||
|
||||
return $requestData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 组装请求参数
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
private function getRequestParams($data = [])
|
||||
{
|
||||
$params = [
|
||||
'ShipperCode' => $data['express_code'],
|
||||
'LogisticCode' => $data['express_no'],
|
||||
];
|
||||
|
||||
if ($data['express_code'] == 'JD') {
|
||||
// 京东青龙配送单号 【好像不用传了,需要验证一下】
|
||||
$params['CustomerName'] = $this->config['jd_code'];
|
||||
} else if ($data['express_code'] == 'SF') {
|
||||
// 收件人手机号后四位
|
||||
$params['CustomerName'] = $data['phone'];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 处理结果
|
||||
*
|
||||
* @param object $response
|
||||
* @param string $msg
|
||||
* @return array
|
||||
*/
|
||||
private function getResponse($result, $msg = '')
|
||||
{
|
||||
$result = json_decode($result, true);
|
||||
if (!$result['Success']) {
|
||||
error_stop($result['Reason'] ?: $msg);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// 加签
|
||||
private function encrypt($data, $app_key)
|
||||
{
|
||||
return urlencode(base64_encode(md5($data . $app_key)));
|
||||
}
|
||||
}
|
||||
45
addons/shopro/library/express/contract/ExpressInterface.php
Normal file
45
addons/shopro/library/express/contract/ExpressInterface.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\express\contract;
|
||||
|
||||
interface ExpressInterface
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* 快递查询
|
||||
*
|
||||
* @param array $data
|
||||
* @param mixed $orderExpress
|
||||
* @return array
|
||||
*/
|
||||
public function search(array $data, $orderExpress = null);
|
||||
|
||||
|
||||
/**
|
||||
* 物流信息订阅
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function subscribe(array $data);
|
||||
|
||||
|
||||
/**
|
||||
* 物流信息推送
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function push(array $data);
|
||||
|
||||
|
||||
/**
|
||||
* 电子面单
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function eOrder(array $data, $items);
|
||||
|
||||
}
|
||||
121
addons/shopro/library/express/provider/Base.php
Normal file
121
addons/shopro/library/express/provider/Base.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\express\provider;
|
||||
|
||||
use addons\shopro\library\express\contract\ExpressInterface;
|
||||
use app\admin\model\shopro\order\Express;
|
||||
use app\admin\model\shopro\order\ExpressLog;
|
||||
|
||||
class Base implements ExpressInterface
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 快递查询
|
||||
*
|
||||
* @param array $data
|
||||
* @param mixed $order_express_id
|
||||
* @return array
|
||||
*/
|
||||
public function search(array $data, $orderExpress = 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 物流信息订阅
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function subscribe(array $data)
|
||||
{
|
||||
error_stop('当前快递驱动不支持物流信息订阅');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 物流信息推送
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function push(array $data)
|
||||
{
|
||||
error_stop('当前快递驱动不支持接受推送');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 电子面单
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function eOrder(array $data, $items)
|
||||
{
|
||||
error_stop('当前快递驱动不支持电子面单');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新包裹信息
|
||||
*
|
||||
* @param array $data
|
||||
* @param mixed $orderExpress
|
||||
* @return array
|
||||
*/
|
||||
protected function updateExpress(array $data, $orderExpress)
|
||||
{
|
||||
// 更新包裹状态
|
||||
if (is_numeric($orderExpress)) {
|
||||
$orderExpress = Express::find($orderExpress);
|
||||
}
|
||||
if ($orderExpress) {
|
||||
$orderExpress->status = $data['status'];
|
||||
$orderExpress->save();
|
||||
|
||||
$this->syncTraces($data['traces'], $orderExpress);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新物流信息
|
||||
*
|
||||
* @param array $traces
|
||||
* @param mixed $orderExpress
|
||||
* @return void
|
||||
*/
|
||||
protected function syncTraces($traces, $orderExpress)
|
||||
{
|
||||
// 查询现有轨迹记录
|
||||
$orderExpressLog = ExpressLog::where('order_express_id', $orderExpress->id)->select();
|
||||
|
||||
$log_count = count($orderExpressLog);
|
||||
if ($log_count > 0) {
|
||||
// 移除已经存在的记录
|
||||
array_splice($traces, 0, $log_count);
|
||||
}
|
||||
|
||||
// 增加包裹记录
|
||||
foreach ($traces as $k => $trace) {
|
||||
$orderExpressLog = new ExpressLog();
|
||||
|
||||
$orderExpressLog->user_id = $orderExpress['user_id'];
|
||||
$orderExpressLog->order_id = $orderExpress['order_id'];
|
||||
$orderExpressLog->order_express_id = $orderExpress['id'];
|
||||
$orderExpressLog->content = $trace['content'];
|
||||
$orderExpressLog->change_date = $trace['change_date'];
|
||||
$orderExpressLog->status = $trace['status'];
|
||||
$orderExpressLog->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
291
addons/shopro/library/express/provider/Kdniao.php
Normal file
291
addons/shopro/library/express/provider/Kdniao.php
Normal file
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\express\provider;
|
||||
|
||||
use think\Log;
|
||||
use think\exception\HttpResponseException;
|
||||
use addons\shopro\library\express\adapter\Kdniao as KdniaoServer;
|
||||
use app\admin\model\shopro\order\Express;
|
||||
use app\admin\model\shopro\order\ExpressLog;
|
||||
|
||||
class Kdniao extends Base
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->server = new KdniaoServer();
|
||||
}
|
||||
|
||||
|
||||
public $status = [
|
||||
'0' => 'noinfo',
|
||||
'1' => 'collect',
|
||||
'2' => 'transport',
|
||||
'201' => 'transport',
|
||||
'202' => 'delivery',
|
||||
'211' => 'delivery',
|
||||
'3' => 'signfor',
|
||||
'301' => 'signfor',
|
||||
'302' => 'signfor',
|
||||
'311' => 'signfor',
|
||||
'4' => 'difficulty',
|
||||
'401' => 'invalid',
|
||||
'402' => 'timeout',
|
||||
'403' => 'timeout',
|
||||
'404' => 'refuse',
|
||||
'412' => 'timeout',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* 快递查询
|
||||
*
|
||||
* @param array $data
|
||||
* @param mixed $orderExpress
|
||||
* @return array
|
||||
*/
|
||||
public function search($data, $orderExpress = 0)
|
||||
{
|
||||
$requestData = $this->formatRequest($data);
|
||||
$result = $this->server->search($requestData);
|
||||
|
||||
$traces = $result['Traces'] ?? [];
|
||||
$status = $result['State'];
|
||||
|
||||
// 格式化结果
|
||||
$formatResult = $this->formatResult([
|
||||
'status' => $status,
|
||||
'traces' => $traces
|
||||
]);
|
||||
|
||||
if ($orderExpress) {
|
||||
$this->updateExpress($formatResult, $orderExpress);
|
||||
}
|
||||
|
||||
return $formatResult;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 物流信息订阅
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function subscribe($data)
|
||||
{
|
||||
$requestData = $this->formatRequest($data);
|
||||
$result = $this->server->subscribe($requestData);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function cancel($data)
|
||||
{
|
||||
$kdniao = sheep_config('shop.dispatch.kdniao');
|
||||
|
||||
$this->server->cancel([
|
||||
'ShipperCode' => $data['express_code'],
|
||||
'OrderCode' => $data['order_code'],
|
||||
'ExpNo' => $data['express_no'],
|
||||
"CustomerName" => $kdniao['customer_name'],
|
||||
"CustomerPwd" => $kdniao['customer_pwd']
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 物流信息推送
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function push(array $data)
|
||||
{
|
||||
$success = true;
|
||||
$reason = '';
|
||||
try {
|
||||
$data = json_decode(html_entity_decode($data['RequestData']), true);
|
||||
$expressData = $data['Data'];
|
||||
|
||||
foreach ($expressData as $key => $express) {
|
||||
$orderExpress = Express::where('express_no', $express['LogisticCode'])->where('express_code', $express['ShipperCode'])->find();
|
||||
|
||||
if (!$orderExpress) {
|
||||
// 包裹不存在,记录日志信息,然后继续下一个
|
||||
Log::error('order-express-notfound:' . json_encode($express));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$express['Success']) {
|
||||
// 失败了
|
||||
if (isset($express['Reason']) && (strpos($express['Reason'], '三天无轨迹') !== false || strpos($express['Reason'], '七天内无轨迹变化') !== false)) {
|
||||
// 需要重新订阅
|
||||
$this->subscribe([
|
||||
'express_code' => $express['ShipperCode'],
|
||||
'express_no' => $express['LogisticCode']
|
||||
]);
|
||||
}
|
||||
|
||||
Log::error('order-express-resubscribe:' . json_encode($express));
|
||||
continue;
|
||||
}
|
||||
|
||||
$traces = $express['Traces'] ?? [];
|
||||
$status = $express['State'];
|
||||
|
||||
// 格式化结果
|
||||
$formatResult = $this->formatResult([
|
||||
'status' => $status,
|
||||
'traces' => $traces
|
||||
]);
|
||||
|
||||
$this->updateExpress($formatResult, $orderExpress);
|
||||
}
|
||||
} catch (HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$reason = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
} catch (\Exception $e) {
|
||||
$success = false;
|
||||
$reason = $e->getMessage();
|
||||
}
|
||||
|
||||
return $this->server->pushResult($success, $reason);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 电子面单
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function eOrder($data, $items)
|
||||
{
|
||||
$kdniao = sheep_config('shop.dispatch.kdniao');
|
||||
if ($kdniao['type'] !== 'vip') {
|
||||
error_stop('仅快递鸟标准版接口支持电子面单功能!');
|
||||
}
|
||||
$consignee = $data['consignee'];
|
||||
$order = $data['order'];
|
||||
$sender = $data['sender'] ?? sheep_config('shop.dispatch.sender');
|
||||
if (empty($sender)) {
|
||||
error_stop('请配置默认发货人信息');
|
||||
}
|
||||
// 运单基础信息
|
||||
$requestData = [
|
||||
// 下面五个参数对应快递鸟的五个参数 https://www.yuque.com/kdnjishuzhichi/dfcrg1/hrfw43
|
||||
"CustomerName" => $kdniao['customer_name'],
|
||||
"CustomerPwd" => $kdniao['customer_pwd'],
|
||||
"MonthCode" => $kdniao['month_code'],
|
||||
"SendSite" => $kdniao['send_site'],
|
||||
"SendStaff" => $kdniao['send_staff'],
|
||||
|
||||
"ShipperCode" => $kdniao['express']['code'] ?? '',
|
||||
"PayType" => $kdniao['pay_type'],
|
||||
"ExpType" => $kdniao['exp_type'],
|
||||
"IsReturnPrintTemplate" => 0, //返回打印面单模板
|
||||
"TemplateSize" => '130', // 一联单
|
||||
"Volume" => 0,
|
||||
"OrderCode" => $order['order_sn'] . '_' . time(), // 商城订单号
|
||||
"Remark" => $order['remark'] ?? '小心轻放' // 备注
|
||||
];
|
||||
|
||||
// 发货人
|
||||
$requestData['Sender'] = [
|
||||
'Name' => $sender['name'],
|
||||
'Mobile' => $sender['mobile'],
|
||||
'ProvinceName' => $sender['province_name'],
|
||||
'CityName' => $sender['city_name'],
|
||||
'ExpAreaName' => $sender['district_name'],
|
||||
'Address' => $sender['address']
|
||||
];
|
||||
|
||||
// 收货人
|
||||
$requestData['Receiver'] = [
|
||||
"Name" => $consignee['consignee'],
|
||||
"Mobile" => $consignee['mobile'],
|
||||
"ProvinceName" => $consignee['province_name'],
|
||||
"CityName" => $consignee['city_name'],
|
||||
"ExpAreaName" => $consignee['district_name'],
|
||||
"Address" => $consignee['address']
|
||||
];
|
||||
|
||||
// 包裹信息
|
||||
$totalCount = 0;
|
||||
$totalWeight = 0;
|
||||
foreach ($items as $k => $item) {
|
||||
$goodsName = $item->goods_title . ($item->goods_sku_text ? '-' . $item->goods_sku_text : '');
|
||||
|
||||
$requestData['Commodity'][] = [
|
||||
"GoodsName" => $goodsName,
|
||||
"Goodsquantity" => $item->goods_num,
|
||||
"GoodsWeight" => $item->goods_num * $item->goods_weight
|
||||
];
|
||||
$totalCount += $item->goods_num;
|
||||
$totalWeight += $item->goods_num * $item->goods_weight;
|
||||
}
|
||||
$requestData['Quantity'] = $totalCount; // 商品数量
|
||||
$requestData['Weight'] = $totalWeight;
|
||||
|
||||
$result = $this->server->eOrder($requestData);
|
||||
if ($result['Success'] === true && $result['ResultCode'] === "100") {
|
||||
return [
|
||||
'code' => $kdniao['express']['code'],
|
||||
'name' => $kdniao['express']['name'],
|
||||
'no' => $result['Order']['LogisticCode'],
|
||||
'ext' => $result,
|
||||
'driver' => 'kdniao'
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 处理请求接口数据
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function formatRequest($data)
|
||||
{
|
||||
$requestData = [
|
||||
'express_code' => $data['express_code'] ?? '',
|
||||
'express_no' => $data['express_no'],
|
||||
'phone' => (isset($data['phone']) && $data['phone']) ? substr($data['phone'], 7) : ''
|
||||
];
|
||||
|
||||
return $requestData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理返回结果
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function formatResult($data)
|
||||
{
|
||||
$status = $this->status[$data['status']] ?? 'noinfo';
|
||||
|
||||
$traces = [];
|
||||
foreach ($data['traces'] as $trace) {
|
||||
$action = $trace['Action'] ?? '';
|
||||
if ($action !== '') {
|
||||
$currentStatus = $this->status[$action] ?? 'noinfo';
|
||||
}
|
||||
$traces[] = [
|
||||
'content' => $trace['AcceptStation'],
|
||||
'change_date' => date('Y-m-d H:i:s', strtotime(substr($trace['AcceptTime'], 0, 19))), // 快递鸟时间格式可能是 2020-08-03 16:58:272 或者 2014/06/25 01:41:06
|
||||
'status' => $currentStatus ?? 'noinfo'
|
||||
];
|
||||
}
|
||||
|
||||
return compact('status', 'traces');
|
||||
}
|
||||
}
|
||||
110
addons/shopro/library/express/provider/Thinkapi.php
Normal file
110
addons/shopro/library/express/provider/Thinkapi.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\express\provider;
|
||||
|
||||
use app\admin\model\shopro\Config;
|
||||
use app\admin\model\shopro\order\Address as OrderAddress;
|
||||
use fast\Http;
|
||||
|
||||
class Thinkapi extends Base
|
||||
{
|
||||
|
||||
protected $uri = 'https://api.topthink.com';
|
||||
|
||||
protected $appCode = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->appCode = Config::getConfigField('shop.dispatch.thinkapi.app_code');
|
||||
}
|
||||
|
||||
|
||||
public $status = [
|
||||
'1' => 'noinfo',
|
||||
'2' => 'transport',
|
||||
'3' => 'delivery',
|
||||
'4' => 'signfor',
|
||||
'5' => 'refuse',
|
||||
'6' => 'difficulty',
|
||||
'7' => 'invalid',
|
||||
'8' => 'timeout',
|
||||
'9' => 'fail',
|
||||
'10' => 'back'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* 快递查询
|
||||
*
|
||||
* @param array $data
|
||||
* @param mixed $orderExpress
|
||||
* @return array
|
||||
*/
|
||||
public function search($data, $orderExpress = 0)
|
||||
{
|
||||
$mobile = (isset($data['mobile']) && $data['mobile']) ? $data['mobile'] : '';
|
||||
if (!$mobile && stripos($data['express_no'], 'SF') === 0 && isset($data['order_id'])) {
|
||||
// 获取手机号
|
||||
$orderAddress = OrderAddress::where('order_id', $data['order_id'])->find();
|
||||
$mobile = $orderAddress ? $orderAddress->mobile : $mobile;
|
||||
}
|
||||
|
||||
$requestData = [
|
||||
'appCode' => $this->appCode,
|
||||
'com' => 'auto',
|
||||
'nu' => $data['express_no'],
|
||||
'phone' => substr($mobile, 7)
|
||||
];
|
||||
|
||||
$result = Http::get($this->uri . '/express/query', $requestData);
|
||||
$result = is_string($result) ? json_decode($result, true) : $result;
|
||||
|
||||
if (isset($result['code']) && $result['code'] != 0) {
|
||||
$msg = $result['data']['msg'] ?? ($result['message'] ?? '');
|
||||
error_stop($msg);
|
||||
}
|
||||
|
||||
$data = $result['data'] ?? [];
|
||||
$traces = $data['data'] ?? [];
|
||||
|
||||
$status = $data['status'];
|
||||
|
||||
// 格式化结果
|
||||
$formatResult = $this->formatResult([
|
||||
'status' => $status,
|
||||
'traces' => $traces
|
||||
]);
|
||||
|
||||
if ($orderExpress) {
|
||||
$this->updateExpress($formatResult, $orderExpress);
|
||||
}
|
||||
|
||||
return $formatResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理返回结果
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function formatResult($data)
|
||||
{
|
||||
$status = $this->status[$data['status']] ?? 'noinfo';
|
||||
|
||||
$traces = [];
|
||||
foreach ($data['traces'] as $trace) {
|
||||
$traces[] = [
|
||||
'content' => $trace['context'],
|
||||
'change_date' => $trace['time'],
|
||||
'status' => $trace['status'] ?? $status
|
||||
];
|
||||
}
|
||||
$traces = array_reverse($traces); // 调转顺序,第一条为最开始运输信息,最后一条为最新消息
|
||||
return compact('status', 'traces');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
565
addons/shopro/library/mplive/Client.php
Normal file
565
addons/shopro/library/mplive/Client.php
Normal file
@@ -0,0 +1,565 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the overtrue/wechat.
|
||||
*
|
||||
* (c) overtrue <i@overtrue.me>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace addons\shopro\library\mplive;
|
||||
|
||||
use EasyWeChat\Kernel\BaseClient;
|
||||
|
||||
/**
|
||||
* Class Client.
|
||||
*
|
||||
* @author Abbotton <uctoo@foxmail.com>
|
||||
*/
|
||||
class Client extends BaseClient
|
||||
{
|
||||
/**
|
||||
* Add broadcast goods.
|
||||
*
|
||||
* @param array $goodsInfo
|
||||
*
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
*
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function create(array $goodsInfo)
|
||||
{
|
||||
$params = [
|
||||
'goodsInfo' => $goodsInfo,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxaapi/broadcast/goods/add', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset audit.
|
||||
*
|
||||
* @param int $auditId
|
||||
* @param int $goodsId
|
||||
*
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
*
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function resetAudit(int $auditId, int $goodsId)
|
||||
{
|
||||
$params = [
|
||||
'auditId' => $auditId,
|
||||
'goodsId' => $goodsId,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxaapi/broadcast/goods/resetaudit', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resubmit audit goods.
|
||||
*
|
||||
* @param int $goodsId
|
||||
*
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
*
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function resubmitAudit(int $goodsId)
|
||||
{
|
||||
$params = [
|
||||
'goodsId' => $goodsId,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxaapi/broadcast/goods/audit', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete broadcast goods.
|
||||
*
|
||||
* @param int $goodsId
|
||||
*
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
*
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function delete(int $goodsId)
|
||||
{
|
||||
$params = [
|
||||
'goodsId' => $goodsId,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxaapi/broadcast/goods/delete', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update goods info.
|
||||
*
|
||||
* @param array $goodsInfo
|
||||
*
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
*
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function update(array $goodsInfo)
|
||||
{
|
||||
$params = [
|
||||
'goodsInfo' => $goodsInfo,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxaapi/broadcast/goods/update', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get goods information and review status.
|
||||
*
|
||||
* @param array $goodsIdArray
|
||||
*
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
*
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function getGoodsWarehouse(array $goodsIdArray)
|
||||
{
|
||||
$params = [
|
||||
'goods_ids' => $goodsIdArray,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxa/business/getgoodswarehouse', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get goods list based on status
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function getApproved(array $params)
|
||||
{
|
||||
return $this->httpGet('wxaapi/broadcast/goods/getapproved', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add goods to the designated live room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function addGoods(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/addgoods', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Room List.
|
||||
*
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
* @author onekb <1@1kb.ren>
|
||||
*/
|
||||
public function getRooms(int $start = 0, int $limit = 10)
|
||||
{
|
||||
$params = [
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxa/business/getliveinfo', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Playback List.
|
||||
*
|
||||
* @param int $roomId
|
||||
* @param int $start
|
||||
* @param int $limit
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
* @author onekb <1@1kb.ren>
|
||||
*/
|
||||
public function getPlaybacks(int $roomId, int $start = 0, int $limit = 10)
|
||||
{
|
||||
$params = [
|
||||
'action' => 'get_replay',
|
||||
'room_id' => $roomId,
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
];
|
||||
|
||||
return $this->httpPostJson('wxa/business/getliveinfo', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a live room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function createLiveRoom(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/create', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a live room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function deleteLiveRoom(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/deleteroom', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a live room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function updateLiveRoom(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/editroom', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the live room push stream url.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getPushUrl(array $params)
|
||||
{
|
||||
return $this->httpGet('wxaapi/broadcast/room/getpushurl', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the live room share qrcode.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getShareQrcode(array $params)
|
||||
{
|
||||
return $this->httpGet('wxaapi/broadcast/room/getsharedcode', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a live room assistant.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function addAssistant(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/addassistant', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a live room assistant.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function updateAssistant(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/modifyassistant', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a live room assistant.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function deleteAssistant(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/removeassistant', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the assistant list.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getAssistantList(array $params)
|
||||
{
|
||||
return $this->httpGet('wxaapi/broadcast/room/getassistantlist', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the sub anchor.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function addSubAnchor(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/addsubanchor', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sub anchor.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function updateSubAnchor(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/modifysubanchor', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the sub anchor.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function deleteSubAnchor(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/deletesubanchor', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sub anchor info.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getSubAnchor(array $params)
|
||||
{
|
||||
return $this->httpGet('wxaapi/broadcast/room/getsubanchor', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn official index on/off.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function updateFeedPublic(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/updatefeedpublic', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn playback status on/off.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function updateReplay(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/updatereplay', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn customer service status on/off.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function updateKf(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/updatekf', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn global comments status on/off.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function updateComment(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/room/updatecomment', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add member role.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function addRole(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/role/addrole', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete member role.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function deleteRole(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/role/deleterole', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role list.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getRoleList(array $params)
|
||||
{
|
||||
return $this->httpGet('wxaapi/broadcast/role/getrolelist', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets long-term subscribers.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function getFollowers(array $params)
|
||||
{
|
||||
return $this->httpPost('wxa/business/get_wxa_followers', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending live broadcast start event to long-term subscribers.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function pushMessage(array $params)
|
||||
{
|
||||
return $this->httpPost('wxa/business/push_message', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of goods on/off shelves in room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function updateGoodsInRoom(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/goods/onsale', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete goods in room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function deleteGoodsInRoom(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/goods/deleteInRoom', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push goods in room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function pushGoods(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/goods/push', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change goods sort in room.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function sortGoods(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/goods/sort', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download goods explanation video.
|
||||
*
|
||||
* @param array $params
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function downloadGoodsExplanationVideo(array $params)
|
||||
{
|
||||
return $this->httpPost('wxaapi/broadcast/goods/getVideo', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小程序scheme码
|
||||
*
|
||||
* @param array $param
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function urlscheme(array $param = [])
|
||||
{
|
||||
return $this->httpPostJson('wxa/generatescheme', $param);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小程序 URL Link
|
||||
*
|
||||
* @param array $param
|
||||
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function urllink(array $param = [])
|
||||
{
|
||||
return $this->httpPostJson('wxa/generate_urllink', $param);
|
||||
}
|
||||
}
|
||||
33
addons/shopro/library/mplive/ServiceProvider.php
Normal file
33
addons/shopro/library/mplive/ServiceProvider.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the overtrue/wechat.
|
||||
*
|
||||
* (c) overtrue <i@overtrue.me>
|
||||
*
|
||||
* This source file is subject to the MIT license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace addons\shopro\library\mplive;
|
||||
|
||||
use Pimple\Container;
|
||||
use Pimple\ServiceProviderInterface;
|
||||
|
||||
/**
|
||||
* Class ServiceProvider.
|
||||
*
|
||||
* @author Abbotton <uctoo@foxmail.com>
|
||||
*/
|
||||
class ServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}.
|
||||
*/
|
||||
public function register(Container $app)
|
||||
{
|
||||
$app['broadcast'] = function ($app) {
|
||||
return new Client($app);
|
||||
};
|
||||
}
|
||||
}
|
||||
70
addons/shopro/library/notify/Notify.php
Normal file
70
addons/shopro/library/notify/Notify.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\notify;
|
||||
|
||||
use think\queue\ShouldQueue;
|
||||
|
||||
class Notify
|
||||
{
|
||||
|
||||
public function sendNotify($notifiables, $notification) {
|
||||
if ($notification instanceof ShouldQueue) {
|
||||
// 队列执行
|
||||
return $this->sendQueueNotify($notifiables, $notification, $notification->delay);
|
||||
}
|
||||
|
||||
return $this->sendNowNotify($notifiables, $notification);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 立即发送
|
||||
*/
|
||||
public function sendNowNotify($notifiables, $notification) {
|
||||
foreach ($notifiables as $key => $notifiable) {
|
||||
$channels = $notification->channels($notifiable);
|
||||
|
||||
if (empty($channels)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($channels as $k => $channel) {
|
||||
(new $channel)->send($notifiable, $notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 队列发送
|
||||
* delay 延迟时间
|
||||
*/
|
||||
public function sendQueueNotify($notifiables, $notification, $delay) {
|
||||
$notifiables = $notifiables instanceof \think\Collection ? $notifiables->all() : collection($notifiables)->all();
|
||||
|
||||
if ($delay > 0) {
|
||||
// 异步延迟发送
|
||||
\think\Queue::later($delay, '\addons\shopro\job\Notification@send', [
|
||||
'notifiables' => $notifiables,
|
||||
'notifiable_name' => get_class(reset($notifiables)),
|
||||
'notification' => $notification,
|
||||
'notification_name' => get_class($notification),
|
||||
], 'shopro');
|
||||
} else {
|
||||
// 异步立即发送
|
||||
\think\Queue::push('\addons\shopro\job\Notification@send', [
|
||||
'notifiables' => $notifiables,
|
||||
'notifiable_name' => get_class(reset($notifiables)),
|
||||
'notification' => $notification,
|
||||
'notification_name' => get_class($notification)
|
||||
], 'shopro');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function __callStatic($name, $arguments)
|
||||
{
|
||||
return (new self)->{$name . 'Notify'}(...$arguments);
|
||||
}
|
||||
}
|
||||
26
addons/shopro/library/notify/traits/Notifiable.php
Normal file
26
addons/shopro/library/notify/traits/Notifiable.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\notify\traits;
|
||||
|
||||
/**
|
||||
* 消息通知 trait
|
||||
*/
|
||||
|
||||
trait Notifiable
|
||||
{
|
||||
public function notify ($notification) {
|
||||
return \addons\shopro\library\notify\Notify::send([$this], $notification);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取 notifiable 身份类型 admin, user
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getNotifiableType()
|
||||
{
|
||||
$notifiable_type = str_replace('\\', '', strtolower(strrchr(static::class, '\\')));
|
||||
return $notifiable_type;
|
||||
}
|
||||
}
|
||||
50
addons/shopro/library/pay/PayService.php
Normal file
50
addons/shopro/library/pay/PayService.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\pay;
|
||||
|
||||
use think\Log;
|
||||
use addons\shopro\library\pay\provider\Base;
|
||||
|
||||
class PayService
|
||||
{
|
||||
protected $payment;
|
||||
protected $platform;
|
||||
|
||||
public function __construct($payment, $platform = null)
|
||||
{
|
||||
$this->payment = $payment;
|
||||
|
||||
$this->platform = $platform ? : request()->header('platform', null);
|
||||
|
||||
if (!$this->platform) {
|
||||
error_stop('缺少用户端平台参数');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 支付提供器
|
||||
*
|
||||
* @param string $type
|
||||
* @return Base
|
||||
*/
|
||||
public function provider($payment = null)
|
||||
{
|
||||
$payment = $payment ?: $this->payment;
|
||||
$class = "\\addons\\shopro\\library\\pay\\provider\\" . \think\helper\Str::studly($payment);
|
||||
if (class_exists($class)) {
|
||||
return new $class($this, $this->platform);
|
||||
}
|
||||
|
||||
error_stop('支付类型不支持');
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function __call($funcname, $arguments)
|
||||
{
|
||||
return $this->provider()->{$funcname}(...$arguments);
|
||||
}
|
||||
|
||||
}
|
||||
234
addons/shopro/library/pay/provider/Alipay.php
Normal file
234
addons/shopro/library/pay/provider/Alipay.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\pay\provider;
|
||||
|
||||
use think\Log;
|
||||
use think\exception\HttpResponseException;
|
||||
use addons\shopro\service\pay\PayRefund;
|
||||
use Yansongda\Pay\Pay;
|
||||
|
||||
class Alipay extends Base
|
||||
{
|
||||
protected $payService = null;
|
||||
protected $pay = null;
|
||||
protected $platform = null;
|
||||
|
||||
public function __construct($payService, $platform = null)
|
||||
{
|
||||
$this->payService = $payService;
|
||||
|
||||
$this->platform = $platform;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function pay($order, $config = [], $from = null)
|
||||
{
|
||||
$this->init('alipay', $config);
|
||||
|
||||
if (in_array($this->platform, ['WechatOfficialAccount', 'WechatMiniProgram', 'H5'])) {
|
||||
// 返回支付宝支付链接
|
||||
if (!$from == 'url') {
|
||||
// return request()->domain() . '/shop/api/pay/alipay?pay_sn=' . $order['out_trade_no'] . '&platform=' . $this->platform . '&return_url=' . urlencode($this->config['alipay']['default']['return_url']);
|
||||
return request()->domain() . '/addons/shopro/pay/alipay?pay_sn=' . $order['out_trade_no'] . '&platform=' . $this->platform . '&return_url=' . urlencode($this->config['alipay']['default']['return_url']);
|
||||
}
|
||||
}
|
||||
|
||||
$method = $this->getMethod('alipay');
|
||||
$result = Pay::alipay()->$method($order);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function transfer($payload, $config = [])
|
||||
{
|
||||
$this->init('alipay', $config);
|
||||
|
||||
$code = 0;
|
||||
$response = Pay::alipay()->transfer($payload);
|
||||
if ($response['code'] === '10000' && $response['status'] === 'SUCCESS') {
|
||||
$code = 1;
|
||||
}
|
||||
|
||||
return [$code, $response];
|
||||
}
|
||||
|
||||
|
||||
public function notify($callback, $config = [])
|
||||
{
|
||||
$this->init('alipay', $config);
|
||||
try {
|
||||
$data = Pay::alipay()->callback(); // 是的,验签就这么简单!
|
||||
|
||||
// { // 支付宝支付成功回调参数
|
||||
// "gmt_create": "2022-06-21 14:54:39",
|
||||
// "charset": "utf-8",
|
||||
// "seller_email": "xptech@qq.com",
|
||||
// "subject": "\u5546\u57ce\u8ba2\u5355\u652f\u4ed8",
|
||||
// "buyer_id": "2088902485164146",
|
||||
// "invoice_amount": "0.01",
|
||||
// "notify_id": "2022062100222145440064141420932810",
|
||||
// "fund_bill_list": "[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
|
||||
// "notify_type": "trade_status_sync",
|
||||
// "trade_status": "TRADE_SUCCESS",
|
||||
// "receipt_amount": "0.01",
|
||||
// "buyer_pay_amount": "0.01",
|
||||
// "app_id": "202*********742",
|
||||
// "seller_id": "2088721922277739",
|
||||
// "gmt_payment": "2022-06-21 14:54:40",
|
||||
// "notify_time": "2022-06-21 14:54:41",
|
||||
// "version": "1.0",
|
||||
// "out_trade_no": "P202202383569762189002100",
|
||||
// "total_amount": "0.01",
|
||||
// "trade_no": "2022062122001464141435375324",
|
||||
// "auth_app_id": "202*********742",
|
||||
// "buyer_logon_id": "157***@163.com",
|
||||
// "point_amount": "0.00"
|
||||
// }
|
||||
|
||||
// { // 支付宝退款成功(交易关闭)回调参数
|
||||
// "gmt_create": "2022-06-21 15:31:34",
|
||||
// "charset": "utf-8",
|
||||
// "seller_email": "xptech@qq.com",
|
||||
// "gmt_payment": "2022-06-21 15:31:34",
|
||||
// "notify_time": "2022-06-21 15:53:32",
|
||||
// "subject": "商城订单支付",
|
||||
// "gmt_refund": "2022-06-21 15:53:32.158",
|
||||
// "out_biz_no": "R202203533190902732002100",
|
||||
// "buyer_id": "2088902485164146",
|
||||
// "version": "1.0",
|
||||
// "notify_id": "2022062100222155332064141421692866",
|
||||
// "notify_type": "trade_status_sync",
|
||||
// "out_trade_no": "P202203305611515511002100",
|
||||
// "total_amount": "0.01",
|
||||
// "trade_status": "TRADE_CLOSED",
|
||||
// "refund_fee": "0.01",
|
||||
// "trade_no": "2022062122001464141435383344",
|
||||
// "auth_app_id": "202*********742",
|
||||
// "buyer_logon_id": "157***@163.com",
|
||||
// "gmt_close": "2022-06-21 15:53:32",
|
||||
// "app_id": "202*********742",
|
||||
// "seller_id": "2088721922277739"
|
||||
// }
|
||||
|
||||
Log::write('pay-notify-origin-data:' . json_encode($data));
|
||||
// 判断是否是支付宝退款(支付宝退款成功会通知该接口)
|
||||
|
||||
$out_trade_no = $data['out_trade_no']; // 商户单号
|
||||
$out_refund_no = $data['out_biz_no'] ?? ''; // 退款单号
|
||||
if (
|
||||
$data['notify_type'] == 'trade_status_sync' // 同步交易状态
|
||||
&& $data['trade_status'] == 'TRADE_CLOSED' // 交易关闭
|
||||
&& $out_refund_no // 退款单号
|
||||
) {
|
||||
// 交给退款实例处理
|
||||
$refund = new PayRefund();
|
||||
$refund->notify([
|
||||
'out_trade_no' => $out_trade_no,
|
||||
'out_refund_no' => $out_refund_no,
|
||||
'payment_json' => json_encode($data)
|
||||
]);
|
||||
|
||||
return Pay::alipay()->success();
|
||||
}
|
||||
|
||||
// 判断支付宝是否是支付成功状态,如果不是,直接返回响应
|
||||
if ($data['trade_status'] != 'TRADE_SUCCESS') {
|
||||
// 不是交易成功的通知,直接返回成功
|
||||
return Pay::alipay()->success();
|
||||
}
|
||||
|
||||
$data['pay_fee'] = $data['total_amount'];
|
||||
$data['transaction_id'] = $data['trade_no'];
|
||||
$data['buyer_info'] = $data['buyer_logon_id'];
|
||||
|
||||
return $callback($data);
|
||||
} catch (HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
format_log_error($e, 'alipayNotify.HttpResponseException', $message);
|
||||
return 'fail';
|
||||
} catch (\Exception $e) {
|
||||
format_log_error($e, 'alipayNotify');
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退款
|
||||
*
|
||||
* @param array $order_data
|
||||
* @param array $config
|
||||
* @return array
|
||||
*/
|
||||
public function refund($order_data, $config = [])
|
||||
{
|
||||
$this->init('alipay', $config);
|
||||
|
||||
$result = Pay::alipay()->refund($order_data);
|
||||
Log::write('pay-refund-origin-data:' . json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// {
|
||||
// "code": "10000",
|
||||
// "msg": "Success",
|
||||
// "buyer_logon_id": "157***@163.com",
|
||||
// "buyer_user_id": "2088902485164146",
|
||||
// "fund_change": "Y",
|
||||
// "gmt_refund_pay": "2022-06-21 15:53:32",
|
||||
// "out_trade_no": "P202203305611515511002100",
|
||||
// "refund_fee": "0.01",
|
||||
// "send_back_fee": "0.00",
|
||||
// "trade_no": "2022062122001464141435383344"
|
||||
// }
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化支付参数
|
||||
*
|
||||
* @param [type] $params
|
||||
* @return void
|
||||
*/
|
||||
protected function formatConfig($config, $data = [])
|
||||
{
|
||||
$config['notify_url'] = request()->domain() . '/addons/shopro/pay/notify/payment/alipay/platform/' . $this->platform;
|
||||
|
||||
if (in_array($this->platform, ['WechatOfficialAccount', 'WechatMiniProgram', 'H5'])) {
|
||||
// app 支付不能带着个参数
|
||||
$config['return_url'] = str_replace('&', '&', request()->param('return_url', ''));
|
||||
}
|
||||
|
||||
$config = $this->formatCert($config);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 拼接支付证书绝对地址
|
||||
*
|
||||
* @param array $config
|
||||
* @return array
|
||||
*/
|
||||
protected function formatCert($config)
|
||||
{
|
||||
$end = substr($config['app_secret_cert'], -4);
|
||||
if ($end == '.crt') {
|
||||
$config['app_secret_cert'] = ROOT_PATH . 'public' . $config['app_secret_cert'];
|
||||
}
|
||||
$config['alipay_public_cert_path'] = ROOT_PATH . 'public' . $config['alipay_public_cert_path'];
|
||||
$config['app_public_cert_path'] = ROOT_PATH . 'public' . $config['app_public_cert_path'];
|
||||
$config['alipay_root_cert_path'] = ROOT_PATH . 'public' . $config['alipay_root_cert_path'];
|
||||
|
||||
// 兼容 epay
|
||||
$config['app_cert_public_key'] = $config['app_public_cert_path'];
|
||||
$config['alipay_root_cert'] = $config['alipay_root_cert_path'];
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
168
addons/shopro/library/pay/provider/Base.php
Normal file
168
addons/shopro/library/pay/provider/Base.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\pay\provider;
|
||||
|
||||
use think\Log;
|
||||
use Yansongda\Pay\Pay;
|
||||
use Yansongda\Pay\Contract\HttpClientInterface;
|
||||
use addons\shopro\facade\HttpClient;
|
||||
use app\admin\model\shopro\PayConfig;
|
||||
|
||||
class Base
|
||||
{
|
||||
|
||||
/**
|
||||
* yansongda 支付示例
|
||||
*
|
||||
* @var Yansongda\Pay\Pay
|
||||
*/
|
||||
protected $pay = null; // yansongda 支付实例
|
||||
|
||||
public $config = null; // 支付参数
|
||||
|
||||
|
||||
/**
|
||||
* yansongda 支付初始化
|
||||
*
|
||||
* @param string $payment
|
||||
* @param array $config
|
||||
* @return Yansongda\Pay\Pay
|
||||
*/
|
||||
public function init($payment, $config = [], $type = 'normal')
|
||||
{
|
||||
$this->config = $this->getConfig($payment, $config, $type);
|
||||
|
||||
$this->pay = Pay::config($this->config);
|
||||
Pay::set(HttpClientInterface::class, HttpClient::instance()); // 使用自定义 client (也继承至 GuzzleHttp\Client)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取支付的所有参数
|
||||
*
|
||||
* @param string $payment
|
||||
* @return array
|
||||
*/
|
||||
protected function getConfig($payment, $config = [], $type = 'normal')
|
||||
{
|
||||
// 获取平台配置
|
||||
$platformConfig = $this->getPlatformConfig();
|
||||
extract($platformConfig);
|
||||
|
||||
$params = $this->getPayConfig($payment, $paymentConfig);
|
||||
|
||||
// 格式化支付参数
|
||||
$params['mode'] = (int)($params['mode'] ?? 0);
|
||||
$params = $this->formatConfig($params, ['app_id' => $app_id], $type);
|
||||
|
||||
// 合并传入的参数
|
||||
$params = array_merge($params, $config);
|
||||
|
||||
// 合并参数
|
||||
$config = $this->baseConfig();
|
||||
$config = array_merge($config, [$payment => ['default' => $params]]);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取平台配置参数
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPlatformConfig()
|
||||
{
|
||||
$platformConfig = sheep_config('shop.platform.' . $this->platform);
|
||||
|
||||
$paymentConfig = $platformConfig['payment'] ?? [];
|
||||
$app_id = $platformConfig['app_id'] ?? '';
|
||||
|
||||
return compact('paymentConfig', 'app_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据平台以及支付方式 获取支付配置表的配置参数
|
||||
*
|
||||
* @param string $payment
|
||||
* @return array
|
||||
*/
|
||||
protected function getPayConfig($payment, $paymentConfig)
|
||||
{
|
||||
$methods = $paymentConfig['methods'];
|
||||
$payment_config = $paymentConfig[$payment] ?? 0;
|
||||
|
||||
if (!in_array($payment, $methods)) {
|
||||
error_stop('当前平台未开启该支付方式');
|
||||
}
|
||||
if ($payment_config) {
|
||||
$payConfig = PayConfig::normal()->where('type', $payment)->find($payment_config);
|
||||
}
|
||||
|
||||
if (!isset($payConfig) || !$payConfig) {
|
||||
error_stop('支付配置参数不存在');
|
||||
}
|
||||
|
||||
return $payConfig->params;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取对应的支付方法名
|
||||
*
|
||||
* @param strign $payment
|
||||
* @return string
|
||||
*/
|
||||
protected function getMethod($payment)
|
||||
{
|
||||
$method = [
|
||||
'wechat' => [
|
||||
'WechatOfficialAccount' => 'mp', //公众号支付 Collection
|
||||
'WechatMiniProgram' => 'mini', //小程序支付 Collection
|
||||
'H5' => 'wap', //手机网站支付 Response
|
||||
'App' => 'app' // APP 支付 JsonResponse
|
||||
],
|
||||
'alipay' => [
|
||||
'WechatOfficialAccount' => 'wap', //手机网站支付 Response
|
||||
'WechatMiniProgram' => 'wap', //小程序支付
|
||||
'H5' => 'wap', //手机网站支付 Response
|
||||
'App' => 'app' //APP 支付 JsonResponse
|
||||
],
|
||||
];
|
||||
|
||||
return $method[$payment][$this->platform];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* yansongda 基础配置
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function baseConfig()
|
||||
{
|
||||
$log_path = RUNTIME_PATH . 'log/pay/';
|
||||
if (!is_dir($log_path)) {
|
||||
@mkdir($log_path, 0755, true);
|
||||
}
|
||||
|
||||
return [
|
||||
'logger' => [ // optional
|
||||
'enable' => true,
|
||||
'file' => $log_path . 'pay.log',
|
||||
'level' => config('app_debug') ? 'debug' : 'info', // 建议生产环境等级调整为 info,开发环境为 debug
|
||||
'type' => 'daily', // optional, 可选 daily.
|
||||
'max_file' => 30, // optional, 当 type 为 daily 时有效,默认 30 天
|
||||
],
|
||||
'http' => [ // optional
|
||||
'timeout' => 5.0,
|
||||
'connect_timeout' => 5.0,
|
||||
// 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
344
addons/shopro/library/pay/provider/Wechat.php
Normal file
344
addons/shopro/library/pay/provider/Wechat.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\pay\provider;
|
||||
|
||||
use think\Log;
|
||||
use think\exception\HttpResponseException;
|
||||
use Yansongda\Pay\Pay;
|
||||
|
||||
class Wechat extends Base
|
||||
{
|
||||
protected $payService = null;
|
||||
protected $platform = null;
|
||||
|
||||
public function __construct($payService, $platform = null)
|
||||
{
|
||||
$this->payService = $payService;
|
||||
|
||||
$this->platform = $platform;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function pay($order, $config = [])
|
||||
{
|
||||
$this->init('wechat', $config);
|
||||
|
||||
if (isset($this->config['wechat']['default']['mode']) && $this->config['wechat']['default']['mode'] === 2) {
|
||||
if (in_array($this->platform, ['WechatOfficialAccount', 'WechatMiniProgram'])) {
|
||||
$order['payer']['sub_openid'] = $order['payer']['openid'] ?? '';
|
||||
unset($order['payer']['openid']);
|
||||
}
|
||||
}
|
||||
|
||||
$order['amount']['total'] = intval(bcmul((string)$order['total_amount'], '100')); // 按分 为单位
|
||||
|
||||
if ($this->platform == 'H5') {
|
||||
$order['_type'] = 'app'; // 使用 配置中的 app_id 字段
|
||||
$order['scene_info'] = [
|
||||
'payer_client_ip' => request()->ip(),
|
||||
'h5_info' => [
|
||||
'type' => 'Wap',
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
unset($order['order_id'], $order['total_amount']);
|
||||
$method = $this->getMethod('wechat');
|
||||
$result = Pay::wechat()->$method($order);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function transfer($payload, $config = [])
|
||||
{
|
||||
$this->init('wechat', $config, 'sub_mch');
|
||||
|
||||
$code = 0;
|
||||
$payload['total_amount'] = intval(bcmul((string)$payload['total_amount'], '100'));
|
||||
|
||||
foreach ($payload['transfer_detail_list'] as $key => &$detail) {
|
||||
$detail['transfer_amount'] = intval(bcmul((string)$detail['transfer_amount'], '100'));
|
||||
}
|
||||
if (isset($this->config['wechat']['default']['_type'])) {
|
||||
// 为了能正常获取 appid
|
||||
$payload['_type'] = $this->config['wechat']['default']['_type'];
|
||||
}
|
||||
|
||||
// $payload['authorization_type'] = 'INFORMATION_AUTHORIZATION_TYPE';
|
||||
$payload['authorization_type'] = 'FUND_AUTHORIZATION_TYPE';
|
||||
// $payload['authorization_type'] = 'INFORMATION_AND_FUND_AUTHORIZATION_TYPE';
|
||||
|
||||
$response = Pay::wechat()->transfer($payload);
|
||||
if (isset($response['batch_id']) && $response['batch_id']) {
|
||||
$code = 1;
|
||||
}
|
||||
|
||||
return [$code, $response];
|
||||
}
|
||||
|
||||
|
||||
public function notify($callback, $config = [])
|
||||
{
|
||||
$this->init('wechat', $config);
|
||||
try {
|
||||
$originData = Pay::wechat()->callback(); // 是的,验签就这么简单!
|
||||
// {
|
||||
// "id": "a5c68a7c-5474-5151-825d-88b4143f8642",
|
||||
// "create_time": "2022-06-20T16:16:12+08:00",
|
||||
// "resource_type": "encrypt-resource",
|
||||
// "event_type": "TRANSACTION.SUCCESS",
|
||||
// "summary": "支付成功",
|
||||
// "resource": {
|
||||
// "original_type": "transaction",
|
||||
// "algorithm": "AEAD_AES_256_GCM",
|
||||
// "ciphertext": {
|
||||
// "mchid": "1623831039",
|
||||
// "appid": "wx43********d3d0",
|
||||
// "out_trade_no": "P202204155176122100021000",
|
||||
// "transaction_id": "4200001433202206201698588194",
|
||||
// "trade_type": "JSAPI",
|
||||
// "trade_state": "SUCCESS",
|
||||
// "trade_state_desc": "支付成功",
|
||||
// "bank_type": "OTHERS",
|
||||
// "attach": "",
|
||||
// "success_time": "2022-06-20T16:16:12+08:00",
|
||||
// "payer": {
|
||||
// "openid": "oRj5A44G6lgCVENzVMxZtoMfNeww"
|
||||
// },
|
||||
// "amount": {
|
||||
// "total": 1,
|
||||
// "payer_total": 1,
|
||||
// "currency": "CNY",
|
||||
// "payer_currency": "CNY"
|
||||
// }
|
||||
// },
|
||||
// "associated_data": "transaction",
|
||||
// "nonce": "qoJzoS9MCNgu"
|
||||
// }
|
||||
// }
|
||||
Log::write('pay-notify-origin-data:' . json_encode($originData));
|
||||
if ($originData['event_type'] == 'TRANSACTION.SUCCESS') {
|
||||
// 支付成功回调
|
||||
$data = $originData['resource']['ciphertext'] ?? [];
|
||||
if (isset($data['trade_state']) && $data['trade_state'] == 'SUCCESS') {
|
||||
// 交易成功
|
||||
$data['pay_fee'] = ($data['amount']['total'] / 100);
|
||||
$data['notify_time'] = date('Y-m-d H:i:s', strtotime((string)$data['success_time']));
|
||||
$data['buyer_info'] = $data['payer']['openid'] ?? ($data['payer']['sub_openid'] ?? '');
|
||||
|
||||
$result = $callback($data, $originData);
|
||||
return $result;
|
||||
}
|
||||
|
||||
return 'fail';
|
||||
} else {
|
||||
// 微信交易未成功,返回 false,让微信再次通知
|
||||
Log::error('notify-error:交易未成功:' . $originData['event_type']);
|
||||
return 'fail';
|
||||
}
|
||||
} catch (HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
format_log_error($e, 'wechatNotify.HttpResponseException', $message);
|
||||
return 'fail';
|
||||
} catch (\Exception $e) {
|
||||
format_log_error($e, 'wechatNotify');
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退款
|
||||
*
|
||||
* @param array $order_data
|
||||
* @param array $config
|
||||
* @return array
|
||||
*/
|
||||
public function refund($order_data, $config = [])
|
||||
{
|
||||
$config['notify_url'] = $config['notify_url'] ?? request()->domain() . '/addons/shopro/pay/notifyRefund/payment/wechat/platform/' . $this->platform;
|
||||
$order_data['notify_url'] = $config['notify_url'];
|
||||
|
||||
$this->init('wechat', $config);
|
||||
|
||||
$order_data['amount']['total'] = intval(bcmul((string)$order_data['amount']['total'], '100'));
|
||||
$order_data['amount']['refund'] = intval(bcmul((string)$order_data['amount']['refund'], '100'));
|
||||
|
||||
$result = Pay::wechat()->refund($order_data);
|
||||
Log::write('pay-refund-origin-data:' . json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
// { 返回数据字段
|
||||
// "amount": {
|
||||
// "currency": "CNY",
|
||||
// "discount_refund": 0,
|
||||
// "from": [],
|
||||
// "payer_refund": 1,
|
||||
// "payer_total": 1,
|
||||
// "refund": 1,
|
||||
// "settlement_refund": 1,
|
||||
// "settlement_total": 1,
|
||||
// "total": 1
|
||||
// },
|
||||
// "channel": "ORIGINAL",
|
||||
// "create_time": "2022-06-20T19:06:36+08:00",
|
||||
// "funds_account": "AVAILABLE",
|
||||
// "out_refund_no": "R202207063668479227002100",
|
||||
// "out_trade_no": "P202205155977315528002100",
|
||||
// "promotion_detail": [],
|
||||
// "refund_id": "50301802252022062021833667769",
|
||||
// "status": "PROCESSING",
|
||||
// "transaction_id": "4200001521202206207964248014",
|
||||
// "user_received_account": "\u652f\u4ed8\u7528\u6237\u96f6\u94b1"
|
||||
// }
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 微信退款回调
|
||||
*
|
||||
* @param array $callback
|
||||
* @param array $config
|
||||
* @return array
|
||||
*/
|
||||
public function notifyRefund($callback, $config = [])
|
||||
{
|
||||
$this->init('wechat', $config);
|
||||
try {
|
||||
$originData = Pay::wechat()->callback(); // 是的,验签就这么简单!
|
||||
Log::write('pay-notifyrefund-callback-data:' . json_encode($originData));
|
||||
// {
|
||||
// "id": "4a553265-1f28-53a3-9395-8d902b902462",
|
||||
// "create_time": "2022-06-21T11:25:33+08:00",
|
||||
// "resource_type": "encrypt-resource",
|
||||
// "event_type": "REFUND.SUCCESS",
|
||||
// "summary": "\u9000\u6b3e\u6210\u529f",
|
||||
// "resource": {
|
||||
// "original_type": "refund",
|
||||
// "algorithm": "AEAD_AES_256_GCM",
|
||||
// "ciphertext": {
|
||||
// "mchid": "1623831039",
|
||||
// "out_trade_no": "P202211233042122753002100",
|
||||
// "transaction_id": "4200001417202206214219765470",
|
||||
// "out_refund_no": "R202211252676008994002100",
|
||||
// "refund_id": "50300002272022062121864292533",
|
||||
// "refund_status": "SUCCESS",
|
||||
// "success_time": "2022-06-21T11:25:33+08:00",
|
||||
// "amount": {
|
||||
// "total": 1,
|
||||
// "refund": 1,
|
||||
// "payer_total": 1,
|
||||
// "payer_refund": 1
|
||||
// },
|
||||
// "user_received_account": "\u652f\u4ed8\u7528\u6237\u96f6\u94b1"
|
||||
// },
|
||||
// "associated_data": "refund",
|
||||
// "nonce": "8xfQknYyLVop"
|
||||
// }
|
||||
// }
|
||||
|
||||
if ($originData['event_type'] == 'REFUND.SUCCESS') {
|
||||
// 支付成功回调
|
||||
$data = $originData['resource']['ciphertext'] ?? [];
|
||||
if (isset($data['refund_status']) && $data['refund_status'] == 'SUCCESS') {
|
||||
// 退款成功
|
||||
$result = $callback($data, $originData);
|
||||
return $result;
|
||||
}
|
||||
|
||||
return 'fail';
|
||||
} else {
|
||||
// 微信交易未成功,返回 false,让微信再次通知
|
||||
Log::error('notify-error:退款未成功:' . $originData['event_type']);
|
||||
return 'fail';
|
||||
}
|
||||
} catch (HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
format_log_error($e, 'wechatNotifyRefund.HttpResponseException', $message);
|
||||
return 'fail';
|
||||
} catch (\Exception $e) {
|
||||
format_log_error($e, 'wechatNotifyRefund');
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 格式化支付参数
|
||||
*
|
||||
* @param [type] $params
|
||||
* @return void
|
||||
*/
|
||||
protected function formatConfig($config, $data = [], $type = 'normal')
|
||||
{
|
||||
if ($config['mode'] == 2 && $type == 'sub_mch') {
|
||||
// 服务商模式,但需要子商户直连 ,重新定义 config(商家转账到零钱)
|
||||
$config = [
|
||||
'mch_id' => $config['sub_mch_id'],
|
||||
'mch_secret_key' => $config['sub_mch_secret_key'],
|
||||
'mch_secret_cert' => $config['sub_mch_secret_cert'],
|
||||
'mch_public_cert_path' => $config['sub_mch_public_cert_path'],
|
||||
];
|
||||
$config['mode'] = 0; // 临时改为普通商户
|
||||
}
|
||||
|
||||
if ($config['mode'] === 2) {
|
||||
// 首先将平台配置的 app_id 初始化到配置中
|
||||
$config['mp_app_id'] = $config['app_id']; // 服务商关联的公众号的 appid
|
||||
$config['sub_app_id'] = $data['app_id']; // 服务商特约子商户
|
||||
} else {
|
||||
$config['app_id'] = $data['app_id'];
|
||||
}
|
||||
|
||||
switch ($this->platform) {
|
||||
case 'WechatMiniProgram':
|
||||
$config['_type'] = 'mini'; // 小程序提现,需要传 _type = mini 才能正确获取到 appid
|
||||
if ($config['mode'] === 2) {
|
||||
$config['sub_mini_app_id'] = $config['sub_app_id'];
|
||||
unset($config['sub_app_id']);
|
||||
} else {
|
||||
$config['mini_app_id'] = $config['app_id'];
|
||||
unset($config['app_id']);
|
||||
}
|
||||
break;
|
||||
case 'WechatOfficialAccount':
|
||||
$config['_type'] = 'mp'; // 小程序提现,需要传 _type = mp 才能正确获取到 appid
|
||||
if ($config['mode'] === 2) {
|
||||
$config['sub_mp_app_id'] = $config['sub_app_id'];
|
||||
unset($config['sub_app_id']);
|
||||
} else {
|
||||
$config['mp_app_id'] = $config['app_id'];
|
||||
unset($config['app_id']);
|
||||
}
|
||||
break;
|
||||
case 'App':
|
||||
case 'H5':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$config['notify_url'] = request()->domain() . '/addons/shopro/pay/notify/payment/wechat/platform/' . $this->platform;
|
||||
$config['mch_secret_cert'] = ROOT_PATH . 'public' . $config['mch_secret_cert'];
|
||||
$config['mch_public_cert_path'] = ROOT_PATH . 'public' . $config['mch_public_cert_path'];
|
||||
|
||||
// 可手动配置微信支付公钥证书
|
||||
$config['wechat_public_cert_id'] = $config['wechat_public_cert_id'] ?? '';
|
||||
$config['wechat_public_cert'] = $config['wechat_public_cert'] ?? '';
|
||||
if ($config['wechat_public_cert_id'] && $config['wechat_public_cert']) {
|
||||
$config['wechat_public_cert_path'] = [
|
||||
$config['wechat_public_cert_id'] => ROOT_PATH . 'public' . $config['wechat_public_cert']
|
||||
];
|
||||
}
|
||||
unset($config['wechat_public_cert_id'], $config['wechat_public_cert']);
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user